210 lines
5.6 KiB
Dart
210 lines
5.6 KiB
Dart
import 'dart:async';
|
|
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<InputSyncPage> createState() => _InputSyncPageState();
|
|
}
|
|
|
|
class _InputSyncPageState extends State<InputSyncPage> {
|
|
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;
|
|
Timer? syncDebounce;
|
|
String lastSyncedText = '';
|
|
bool connected = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadSavedConnection();
|
|
inputController.addListener(_syncTextChange);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
syncDebounce?.cancel();
|
|
channel?.sink.close();
|
|
hostController.dispose();
|
|
portController.dispose();
|
|
inputController.dispose();
|
|
inputFocusNode.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _loadSavedConnection() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
hostController.text = prefs.getString(_hostKey) ?? '';
|
|
portController.text = prefs.getString(_portKey) ?? '8181';
|
|
}
|
|
|
|
Future<void> _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;
|
|
});
|
|
_sendCurrentText(force: true);
|
|
inputFocusNode.requestFocus();
|
|
}
|
|
|
|
void _disconnect() {
|
|
syncDebounce?.cancel();
|
|
channel?.sink.close();
|
|
channel = null;
|
|
setState(() {
|
|
connected = false;
|
|
});
|
|
}
|
|
|
|
void _syncTextChange() {
|
|
if (!connected || channel == null) {
|
|
return;
|
|
}
|
|
|
|
final value = inputController.value;
|
|
if (value.composing.isValid) {
|
|
return;
|
|
}
|
|
|
|
final currentText = value.text;
|
|
if (currentText == lastSyncedText) {
|
|
return;
|
|
}
|
|
|
|
syncDebounce?.cancel();
|
|
syncDebounce = Timer(const Duration(milliseconds: 800), _sendCurrentText);
|
|
}
|
|
|
|
void _sendCurrentText({bool force = false}) {
|
|
if (!connected || channel == null) {
|
|
return;
|
|
}
|
|
|
|
final latestValue = inputController.value;
|
|
if (latestValue.composing.isValid) {
|
|
return;
|
|
}
|
|
|
|
if (!force && latestValue.text == lastSyncedText) {
|
|
return;
|
|
}
|
|
|
|
lastSyncedText = latestValue.text;
|
|
_send('replaceAll', text: latestValue.text);
|
|
}
|
|
|
|
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',
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|