Token management, Factory reset, UX/UI Improvements
This commit is contained in:
+261
-97
@@ -1,11 +1,15 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:liteauthconfig/models/appconfig.dart';
|
||||
import 'package:liteauthconfig/l10n/app_localizations.dart';
|
||||
import 'package:liteauthconfig/models/device_status.dart';
|
||||
import 'package:liteauthconfig/models/logentry.dart';
|
||||
import 'package:liteauthconfig/pages/reconfigure_network.dart';
|
||||
import 'package:liteauthconfig/pages/tokeninfo.dart';
|
||||
import 'package:liteauthconfig/utils/network.dart';
|
||||
|
||||
class DeviceInfoPage extends StatefulWidget {
|
||||
@@ -17,13 +21,18 @@ class DeviceInfoPage extends StatefulWidget {
|
||||
State<StatefulWidget> createState() => _DeviceInfoPageState();
|
||||
}
|
||||
|
||||
enum TokenState { requesting, exists, error }
|
||||
|
||||
class _DeviceInfoPageState extends State<DeviceInfoPage> {
|
||||
late Dio httpClient;
|
||||
late Device device;
|
||||
DeviceStatus? deviceStatus;
|
||||
String statusMessage = "";
|
||||
bool online = false;
|
||||
bool loading = false;
|
||||
Timer? refreshTimer;
|
||||
late Timer logPollTimer;
|
||||
List<LogEntry> logEntries = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -31,40 +40,67 @@ class _DeviceInfoPageState extends State<DeviceInfoPage> {
|
||||
|
||||
refreshDevice();
|
||||
checkStatus();
|
||||
logPollTimer = Timer.periodic(
|
||||
Duration(seconds: 15),
|
||||
(timer) => queryLogs(),
|
||||
);
|
||||
}
|
||||
|
||||
void refreshDevice() {
|
||||
device = AppConfig().devices[widget.deviceIndex];
|
||||
}
|
||||
|
||||
Future checkToken() async {
|
||||
var appLocal = AppLocalizations.of(context)!;
|
||||
var resp = await httpClient.get("https://liteauth.local/api/getToken");
|
||||
|
||||
if (resp.statusCode != 200) {
|
||||
statusMessage = appLocal.tokenForbidden;
|
||||
return;
|
||||
}
|
||||
|
||||
device.token = resp.data;
|
||||
final config = AppConfig();
|
||||
config.devices[widget.deviceIndex] = device;
|
||||
await config.save();
|
||||
}
|
||||
|
||||
Future checkStatus() async {
|
||||
statusMessage = "";
|
||||
|
||||
setState(() {
|
||||
loading = true;
|
||||
});
|
||||
var client = await createEspHttpClient();
|
||||
httpClient = await createEspHttpClient(token: device.token);
|
||||
|
||||
try {
|
||||
var resp = await client.get(
|
||||
Uri.parse("https://liteauth.local/api/status"),
|
||||
);
|
||||
deviceStatus = DeviceStatus.fromJson(jsonDecode(resp.body));
|
||||
online = resp.statusCode >= 200 && resp.statusCode < 300;
|
||||
var resp = await httpClient.get("https://liteauth.local/api/status");
|
||||
deviceStatus = DeviceStatus.fromJson(resp.data);
|
||||
online = resp.statusCode! >= 200 && resp.statusCode! < 300;
|
||||
} catch (e) {
|
||||
statusMessage = e.toString();
|
||||
online = false;
|
||||
refreshTimer ??= Timer.periodic(const Duration(seconds: 5), (
|
||||
refreshTimer ??= Timer.periodic(const Duration(seconds: 10), (
|
||||
timer,
|
||||
) async {
|
||||
if (loading) return;
|
||||
await checkStatus();
|
||||
if (online) {
|
||||
statusMessage = "";
|
||||
timer.cancel();
|
||||
refreshTimer = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (online && device.token.isEmpty) {
|
||||
try {
|
||||
await checkToken();
|
||||
} catch (e) {
|
||||
statusMessage = e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
loading = false;
|
||||
@@ -75,28 +111,96 @@ class _DeviceInfoPageState extends State<DeviceInfoPage> {
|
||||
void factoryReset() async {
|
||||
var appLocal = AppLocalizations.of(context)!;
|
||||
|
||||
var reset = await showDialog<bool>(context: context, builder: (ctx) => AlertDialog.adaptive(
|
||||
title: Text(appLocal.factoryReset),
|
||||
content: Text(appLocal.resetConfirm),
|
||||
actions: [
|
||||
TextButton(onPressed: () {
|
||||
Navigator.of(ctx).pop(false);
|
||||
}, child: Text(appLocal.cancel)),
|
||||
TextButton(onPressed: () {
|
||||
Navigator.of(ctx).pop(true);
|
||||
}, child: Text(appLocal.reset)),
|
||||
],
|
||||
)) ?? false;
|
||||
var reset =
|
||||
await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog.adaptive(
|
||||
title: Text(appLocal.factoryReset),
|
||||
content: Text(appLocal.resetConfirm),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop(false);
|
||||
},
|
||||
child: Text(appLocal.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop(true);
|
||||
},
|
||||
child: Text(appLocal.reset),
|
||||
),
|
||||
],
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
|
||||
if (!reset) return;
|
||||
|
||||
var client = await createEspHttpClient();
|
||||
client.get(Uri.parse("https://liteauth.local/api/factoryReset"));
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.pop(context);
|
||||
|
||||
setState(() {
|
||||
loading = true;
|
||||
});
|
||||
final resp = await httpClient.get(
|
||||
"https://liteauth.local/api/factoryReset",
|
||||
);
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
|
||||
if (resp.statusCode != 200 && mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(appLocal.resetFailedCode(resp.statusCode!))),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(appLocal.resetSuccessful)));
|
||||
}
|
||||
|
||||
device.token = "";
|
||||
var config = AppConfig();
|
||||
config.devices[widget.deviceIndex] = device;
|
||||
await config.save();
|
||||
reconfigureNetwork();
|
||||
}
|
||||
|
||||
void queryLogs() async {
|
||||
if (!online) return;
|
||||
|
||||
setState(() {
|
||||
loading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
var resp = await httpClient.get("https://liteauth.local/api/logs");
|
||||
print(DateTime.now().toIso8601String());
|
||||
logEntries = (resp.data as List<dynamic>).reversed
|
||||
.map((data) => LogEntry.fromJson(data))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
refreshTimer?.cancel();
|
||||
logPollTimer.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -115,81 +219,7 @@ class _DeviceInfoPageState extends State<DeviceInfoPage> {
|
||||
)
|
||||
: null,
|
||||
actions: [
|
||||
MenuAnchor(
|
||||
builder: (context, controller, child) {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
}
|
||||
},
|
||||
icon: Icon(Icons.adaptive.more),
|
||||
);
|
||||
},
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(Icons.refresh),
|
||||
onPressed: checkStatus,
|
||||
child: Text(appLocal.refresh),
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(Icons.network_wifi),
|
||||
child: Text(appLocal.reconfigureNetwork),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
ReconfigureNetworkPage(widget.deviceIndex),
|
||||
),
|
||||
).then((result) {
|
||||
if (result) {
|
||||
refreshDevice();
|
||||
checkStatus();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(Icons.history),
|
||||
child: Text(appLocal.factoryReset),
|
||||
onPressed: factoryReset,
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(Icons.delete),
|
||||
child: Text(appLocal.delete),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog.adaptive(
|
||||
title: Text(appLocal.deletion),
|
||||
content: Text(appLocal.deviceDeleteConfirm),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(appLocal.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
AppConfig()
|
||||
..devices.removeAt(widget.deviceIndex)
|
||||
..save();
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(appLocal.delete),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(onPressed: moreActions, icon: Icon(Icons.adaptive.more)),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
@@ -251,6 +281,25 @@ class _DeviceInfoPageState extends State<DeviceInfoPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: logEntries.length,
|
||||
itemBuilder: (ctx, idx) {
|
||||
var entry = logEntries[idx];
|
||||
return Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(
|
||||
// DateFormat.yMd().add_jms().format(
|
||||
// entry.time.toLocal(),
|
||||
// ),
|
||||
entry.time.toIso8601String(),
|
||||
),
|
||||
Text(entry.uid),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -259,4 +308,119 @@ class _DeviceInfoPageState extends State<DeviceInfoPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void reconfigureNetwork() {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ReconfigureNetworkPage(widget.deviceIndex),
|
||||
),
|
||||
).then((result) {
|
||||
if (result == true) {
|
||||
refreshDevice();
|
||||
checkStatus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void moreActions() {
|
||||
final appTheme = Theme.of(context);
|
||||
final appLocal = AppLocalizations.of(context)!;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (ctx) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
child: Text(
|
||||
appLocal.additionalActions,
|
||||
style: appTheme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.refresh),
|
||||
onTap: () {
|
||||
if (online) {
|
||||
queryLogs();
|
||||
} else {
|
||||
checkStatus();
|
||||
}
|
||||
Navigator.pop(context);
|
||||
},
|
||||
title: Text(appLocal.refresh),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.key),
|
||||
onTap: () {
|
||||
Navigator.of(ctx).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
TokenInfo(widget.deviceIndex, device.token),
|
||||
),
|
||||
);
|
||||
},
|
||||
title: Text(appLocal.manageToken),
|
||||
),
|
||||
Visibility(
|
||||
visible: Platform.isAndroid || Platform.isIOS,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.network_wifi),
|
||||
title: Text(appLocal.reconfigureNetwork),
|
||||
onTap: reconfigureNetwork,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
|
||||
child: Text(
|
||||
appLocal.dangerousActions,
|
||||
style: appTheme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.history),
|
||||
title: Text(appLocal.factoryReset),
|
||||
onTap: factoryReset,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete),
|
||||
title: Text(appLocal.delete),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog.adaptive(
|
||||
title: Text(appLocal.deletion),
|
||||
content: Text(appLocal.deviceDeleteConfirm),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(appLocal.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
AppConfig()
|
||||
..devices.removeAt(widget.deviceIndex)
|
||||
..save();
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(appLocal.delete),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ class _DeviceListPageState extends State<DeviceListPage> {
|
||||
if (!(Platform.isAndroid || Platform.isIOS)) return;
|
||||
|
||||
availability = await NfcManager.instance.checkAvailability();
|
||||
setState(() {});
|
||||
|
||||
if (availability != NfcAvailability.enabled) return;
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
|
||||
class QRScanner extends StatelessWidget {
|
||||
const QRScanner({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MobileScanner(
|
||||
onDetect: (result) {
|
||||
Navigator.pop(context, result);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
+21
-19
@@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:liteauthconfig/models/appconfig.dart';
|
||||
import 'package:liteauthconfig/dialogs/esptouchdialog.dart';
|
||||
import 'package:liteauthconfig/l10n/app_localizations.dart';
|
||||
import 'package:multicast_dns/multicast_dns.dart';
|
||||
import 'package:network_info_plus/network_info_plus.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
@@ -44,7 +43,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
var appLocal = AppLocalizations.of(context)!;
|
||||
var appTheme = Theme.of(context);
|
||||
|
||||
if (await Permission.location.isDenied) {
|
||||
if ((Platform.isAndroid || Platform.isIOS) && await Permission.location.isDenied) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog.adaptive(
|
||||
@@ -70,7 +69,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
],
|
||||
),
|
||||
);
|
||||
} else if (await Permission.location.isGranted) {
|
||||
} else if (!(Platform.isAndroid || Platform.isIOS) || await Permission.location.isGranted) {
|
||||
fetchNetworkInfo();
|
||||
}
|
||||
}
|
||||
@@ -95,12 +94,29 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
}
|
||||
|
||||
void submit() async {
|
||||
var appLocal = AppLocalizations.of(context)!;
|
||||
var name = nameController.text.isEmpty ? ssidController.text : nameController.text;
|
||||
|
||||
if (ssidController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(
|
||||
appLocal.cannotBeEmpty("SSID")
|
||||
),));
|
||||
return;
|
||||
}
|
||||
|
||||
if (bssidController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(
|
||||
appLocal.cannotBeEmpty("BSSID")
|
||||
),));
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.setup) {
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => ESPTouchDialog(
|
||||
name: nameController.text,
|
||||
name: name,
|
||||
ssid: ssidController.text,
|
||||
bssid: bssidController.text,
|
||||
password: passwordController.text,
|
||||
@@ -109,26 +125,12 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
);
|
||||
} else {
|
||||
final dev = Device(
|
||||
name: nameController.text,
|
||||
name: name,
|
||||
routerSsid: ssidController.text,
|
||||
routerBssid: bssidController.text,
|
||||
networkPassword: passwordController.text,
|
||||
);
|
||||
|
||||
final MDnsClient client = MDnsClient();
|
||||
await client.start();
|
||||
|
||||
// TODO: Change to dynamic mDNS name
|
||||
String name = "liteauth.local";
|
||||
|
||||
await for (final PtrResourceRecord ptr
|
||||
in client.lookup<PtrResourceRecord>(
|
||||
ResourceRecordQuery.serverPointer(name),
|
||||
)) {
|
||||
await for (final SrvResourceRecord srv in client.lookup(ResourceRecordQuery.service(ptr.domainName))) {
|
||||
// TODO: Actually implement record lookup
|
||||
}
|
||||
}
|
||||
AppConfig().devices.add(dev);
|
||||
}
|
||||
if (context.mounted) {
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:liteauthconfig/l10n/app_localizations.dart';
|
||||
import 'package:liteauthconfig/models/appconfig.dart';
|
||||
import 'package:liteauthconfig/pages/qrscanner.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
class TokenInfo extends StatefulWidget {
|
||||
final int deviceIndex;
|
||||
final String token;
|
||||
|
||||
const TokenInfo(this.deviceIndex, this.token, {super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TokenInfoState();
|
||||
}
|
||||
|
||||
class _TokenInfoState extends State<TokenInfo> {
|
||||
TextEditingController tokenController = TextEditingController();
|
||||
bool showToken = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
tokenController.text = widget.token;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future saveToken() async {
|
||||
final appLocal = AppLocalizations.of(context)!;
|
||||
var config = AppConfig();
|
||||
|
||||
config.devices[widget.deviceIndex].token =
|
||||
tokenController.text;
|
||||
await config.save();
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(appLocal.saved)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future scanQrCode() async {
|
||||
var code = await Navigator.push<BarcodeCapture>(
|
||||
context,
|
||||
MaterialPageRoute(builder: (ctx) => QRScanner()),
|
||||
);
|
||||
var value = code?.barcodes.first.rawValue;
|
||||
|
||||
if (code == null || value == null) return;
|
||||
|
||||
tokenController.text = value;
|
||||
await saveToken();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appLocal = AppLocalizations.of(context)!;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(appLocal.token)),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsetsGeometry.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(appLocal.viewTokenWarn),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Checkbox.adaptive(
|
||||
semanticLabel: appLocal.showToken,
|
||||
value: showToken,
|
||||
onChanged: (newValue) {
|
||||
setState(() {
|
||||
showToken = newValue!;
|
||||
});
|
||||
},
|
||||
),
|
||||
Text(appLocal.showToken),
|
||||
],
|
||||
),
|
||||
|
||||
Visibility(
|
||||
visible: showToken,
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Visibility(
|
||||
visible: tokenController.text.isNotEmpty,
|
||||
replacement: Text(appLocal.tokenNotFound),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox.square(
|
||||
dimension: 200,
|
||||
child: QrImageView(
|
||||
data: tokenController.text,
|
||||
backgroundColor: Colors.white,
|
||||
padding: EdgeInsets.all(16),
|
||||
),
|
||||
),
|
||||
Gap(16),
|
||||
OutlinedButton(
|
||||
onPressed: () async {
|
||||
await Clipboard.setData(
|
||||
ClipboardData(text: tokenController.text),
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(appLocal.copied)),
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.copy),
|
||||
Gap(8),
|
||||
Text(appLocal.copyToken),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Gap(16),
|
||||
TextField(
|
||||
controller: tokenController,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: appLocal.token,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: Platform.isLinux || Platform.isWindows
|
||||
? null
|
||||
: scanQrCode,
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
),
|
||||
),
|
||||
onChanged: (newValue) {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
Gap(16),
|
||||
FilledButton(
|
||||
onPressed: tokenController.text == widget.token
|
||||
? null
|
||||
: saveToken,
|
||||
child: Text(appLocal.saveToken),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user