[feat] add i18n localization: en/zh; add peers display; ui refresh

This commit is contained in:
eventlOwOp
2025-04-06 20:20:14 +08:00
parent 0405277231
commit bacea863bf
22 changed files with 3487 additions and 446 deletions
+19 -304
View File
@@ -1,14 +1,16 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'dart:io';
import 'dart:developer' as developer;
import 'package:flutter/services.dart';
import 'package:quickalert/quickalert.dart';
import './authed_client.dart';
import 'package:path_provider/path_provider.dart';
import './screens/home_page.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import './l10n/app_localizations.dart';
void main() {
// Add global error handling
FlutterError.onError = (FlutterErrorDetails details) {
FlutterError.presentError(details);
developer.log('Application error', error: details.toString());
};
runApp(const MyApp());
}
@@ -19,6 +21,16 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'ZeroTier for Magisk',
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('en'),
Locale('zh'),
],
theme: ThemeData(
colorSchemeSeed: Colors.lightBlue,
useMaterial3: true,
@@ -34,300 +46,3 @@ class MyApp extends StatelessWidget {
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
Future<void> zerotierCommand(String command) async {
final path = (await getApplicationDocumentsDirectory()).path;
final file = File('$path/run/pipe');
await file.writeAsString(command);
}
class _MyHomePageState extends State<MyHomePage> {
List<String> networkList = [];
bool runningStatus = false;
final AuthedClient authed = AuthedClient();
loadNetwork() async {
final client = await authed.client;
final resp = await client.get(
Uri.http('localhost:9993', 'network'),
);
final body = jsonDecode(resp.body);
networkList = List<String>.from(body.map((u) => u['id']));
}
void leaveNetwork(String id) async {
final client = await authed.client;
final resp = await client.delete(Uri.http('localhost:9993', 'network/$id'));
if (resp.statusCode != 200) {
// failed
return;
}
// success
setState(() {
networkList.remove(id);
});
}
Future<void> joinNetwork(String id) async {
final client = await authed.client;
final resp = await client.put(Uri.http('localhost:9993', 'network/$id'));
if (resp.statusCode != 200) {
// failed
return;
}
// success
if (!networkList.contains(id)) {
setState(() {
networkList.add(id);
});
}
}
Future<void> loadStatus() async {
final client = await authed.client;
try {
await client.get(Uri.http('localhost:9993'));
setState(() => runningStatus = true);
return;
} on SocketException {
setState(() => runningStatus = false);
} catch (e) {
developer.log('error', error: e.toString());
// error
}
}
late Future<void> loadingNetworkFuture = loadNetwork();
@override
void initState() {
super.initState();
loadStatus();
}
Function cmdButtonWrapper(func) {
bool doing = false;
return () => doing
? null
: () async {
setState(() => doing = true);
await func();
setState(() => doing = false);
};
}
late Function restartFn, startFn, stopFn, joinFn, leaveFn;
_MyHomePageState() : super() {
restartFn = cmdButtonWrapper(() => zerotierCommand('restart'));
startFn = cmdButtonWrapper(() => zerotierCommand('start'));
stopFn = cmdButtonWrapper(() => zerotierCommand('stop'));
joinFn = cmdButtonWrapper(() => joinNetwork(_idInputController.text));
}
final _idInputController = TextEditingController();
homePage({bool title = false}) {
if (title) {
return 'Status';
}
return Column(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Builder(builder: (BuildContext context) {
final color = (runningStatus ? Colors.green : Colors.red)[300];
return Column(children: [
Icon(runningStatus ? Icons.play_arrow : Icons.stop,
color: color, size: 72),
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Text(runningStatus ? 'Running' : 'Stopped',
style: TextStyle(color: color, fontSize: 18)),
IconButton(icon: const Icon(Icons.refresh), onPressed: loadStatus)
])
]);
}),
IntrinsicWidth(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
OutlinedButton.icon(
style: const ButtonStyle(alignment: Alignment.centerLeft),
onPressed: startFn(),
label: const Text('Start'),
icon: const Icon(Icons.play_arrow),
),
OutlinedButton.icon(
style: const ButtonStyle(alignment: Alignment.centerLeft),
onPressed: restartFn(),
label: const Text('Restart'),
icon: const Icon(Icons.restart_alt),
),
OutlinedButton.icon(
style: const ButtonStyle(alignment: Alignment.centerLeft),
onPressed: stopFn(),
label: const Text('Stop'),
icon: const Icon(Icons.stop),
),
],
))
]);
}
networkPage({bool title = false}) {
if (title) {
return 'Network';
}
return Padding(
padding: const EdgeInsets.all(12),
child: Column(
children: [
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
SizedBox(
width: 200,
child: TextField(
controller: _idInputController,
textAlign: TextAlign.center,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.allow(RegExp('[0-9a-f]'))
],
maxLength: 16,
decoration: const InputDecoration(
labelText: "network id", icon: Icon(Icons.router)))),
OutlinedButton.icon(
onPressed: () {
joinFn()();
QuickAlert.show(
context: context,
type: QuickAlertType.success,
text: 'Joined ${_idInputController.text}',
);
},
label: const Text('Join'),
icon: const Icon(Icons.add)),
]),
const SizedBox(height: 30),
const Divider(height: 10),
ListTile(
title: const Text('Networks'),
titleTextStyle: const TextStyle(fontSize: 18),
trailing: IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
setState(() {
loadingNetworkFuture = loadNetwork();
});
})),
const SizedBox(height: 10),
FutureBuilder<void>(
future: loadingNetworkFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
return ListView.builder(
shrinkWrap: true,
itemCount: networkList.length,
itemBuilder: (BuildContext ctx, int i) {
return Card(
child: ListTile(
title: Text(networkList[i]),
trailing: SizedBox(
width: 96,
child: Row(children: [
IconButton(
onPressed: () {
Clipboard.setData(ClipboardData(
text: networkList[i]));
QuickAlert.show(
context: context,
type: QuickAlertType.success,
text: 'Copied to clipboard!',
);
},
tooltip: '复制',
icon: const Icon(Icons.copy)),
IconButton(
onPressed: () => QuickAlert.show(
context: context,
type: QuickAlertType.confirm,
text:
'Delete network ${networkList[i]} ?',
confirmBtnText: 'Yes',
cancelBtnText: 'No',
confirmBtnColor: Colors.green,
onConfirmBtnTap: () {
leaveNetwork(networkList[i]);
Navigator.pop(context);
}),
tooltip: '离开',
icon: const Icon(Icons.close)),
]))));
});
}
return const CircularProgressIndicator();
},
),
],
));
}
peersPage({bool title = false}) {
if (title) {
return 'Peers';
}
return Container();
}
int currentPageIndex = 0;
genWidgetList({bool title = false}) {
return [
homePage(title: title),
networkPage(title: title),
peersPage(title: title)
][currentPageIndex];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(genWidgetList(title: true))),
bottomNavigationBar: NavigationBar(
onDestinationSelected: (int index) {
setState(() {
currentPageIndex = index;
});
},
selectedIndex: currentPageIndex,
destinations: const <Widget>[
NavigationDestination(
selectedIcon: Icon(Icons.home),
icon: Icon(Icons.home_outlined),
label: 'Status',
),
NavigationDestination(
selectedIcon: Icon(Icons.router),
icon: Icon(Icons.router_outlined),
label: 'Network',
),
NavigationDestination(
selectedIcon: Icon(Icons.account_box),
icon: Icon(Icons.account_box_outlined),
label: 'Peers',
),
],
),
body: Center(
child: genWidgetList(),
));
}
}