[feat] add i18n localization: en/zh; add peers display; ui refresh
This commit is contained in:
+19
-304
@@ -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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user