Initialize Android client skeleton
This commit is contained in:
187
lib/main.dart
Normal file
187
lib/main.dart
Normal file
@@ -0,0 +1,187 @@
|
||||
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',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user