Files
android/lib/main.dart
2026-05-15 20:12:07 +08:00

188 lines
5.3 KiB
Dart

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;
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<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;
});
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',
),
),
),
],
),
),
);
}
}