import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; void main() { runApp(const WirelessTextSyncerApp()); } class WirelessTextSyncerApp extends StatelessWidget { const WirelessTextSyncerApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'WirelessTextSyncer', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF2563EB)), useMaterial3: true, ), home: const InputSyncPage(), ); } } class InputSyncPage extends StatefulWidget { const InputSyncPage({super.key}); @override State createState() => _InputSyncPageState(); } class _InputSyncPageState extends State { static const _hostKey = 'server_host'; static const _portKey = 'server_port'; final hostController = TextEditingController(); final portController = TextEditingController(text: '8181'); final inputController = TextEditingController(); final inputFocusNode = FocusNode(); WebSocketChannel? channel; String lastText = ''; bool connected = false; @override void initState() { super.initState(); _loadSavedConnection(); inputController.addListener(_syncTextChange); } @override void dispose() { channel?.sink.close(); hostController.dispose(); portController.dispose(); inputController.dispose(); inputFocusNode.dispose(); super.dispose(); } Future _loadSavedConnection() async { final prefs = await SharedPreferences.getInstance(); hostController.text = prefs.getString(_hostKey) ?? ''; portController.text = prefs.getString(_portKey) ?? '8181'; } Future _connect() async { final host = hostController.text.trim(); final port = portController.text.trim(); if (host.isEmpty || port.isEmpty) { return; } final prefs = await SharedPreferences.getInstance(); await prefs.setString(_hostKey, host); await prefs.setString(_portKey, port); channel?.sink.close(); channel = WebSocketChannel.connect(Uri.parse('ws://$host:$port')); setState(() { connected = true; }); inputFocusNode.requestFocus(); } void _disconnect() { channel?.sink.close(); channel = null; setState(() { connected = false; }); } void _syncTextChange() { final currentText = inputController.text; if (!connected || channel == null || currentText == lastText) { lastText = currentText; return; } if (currentText.length > lastText.length && currentText.startsWith(lastText)) { _send('insertText', text: currentText.substring(lastText.length)); } else if (lastText.length == currentText.length + 1 && lastText.startsWith(currentText)) { _send('backspace'); } else { _send('insertText', text: currentText); } lastText = currentText; } void _send(String action, {String? text}) { channel?.sink.add(jsonEncode({'action': action, 'text': ?text})); } @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Column( children: [ Padding( padding: const EdgeInsets.all(12), child: Row( children: [ Expanded( flex: 3, child: TextField( controller: hostController, decoration: const InputDecoration( labelText: 'Windows IP', border: OutlineInputBorder(), isDense: true, ), keyboardType: TextInputType.number, ), ), const SizedBox(width: 8), Expanded( child: TextField( controller: portController, decoration: const InputDecoration( labelText: 'Port', border: OutlineInputBorder(), isDense: true, ), keyboardType: TextInputType.number, ), ), const SizedBox(width: 8), IconButton.filledTonal( tooltip: connected ? 'Disconnect' : 'Connect', onPressed: connected ? _disconnect : _connect, icon: Icon(connected ? Icons.link_off : Icons.link), ), ], ), ), Expanded( child: TextField( controller: inputController, focusNode: inputFocusNode, autofocus: true, expands: true, maxLines: null, minLines: null, textAlignVertical: TextAlignVertical.top, decoration: const InputDecoration( border: InputBorder.none, contentPadding: EdgeInsets.all(16), hintText: 'Type here', ), ), ), ], ), ), ); } }