import 'dart:async'; 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 { final int deviceIndex; const DeviceInfoPage({super.key, required this.deviceIndex}); @override State createState() => _DeviceInfoPageState(); } enum TokenState { requesting, exists, error } class _DeviceInfoPageState extends State { late Dio httpClient; late Device device; DeviceStatus? deviceStatus; String statusMessage = ""; bool online = false; bool loading = false; Timer? refreshTimer; late Timer logPollTimer; List logEntries = []; @override void initState() { super.initState(); 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; }); httpClient = await createEspHttpClient(token: device.token); try { 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: 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; }); } } void factoryReset() async { var appLocal = AppLocalizations.of(context)!; var reset = await showDialog( 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; 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).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(); } @override Widget build(BuildContext context) { final appTheme = Theme.of(context); final appLocal = AppLocalizations.of(context)!; return Scaffold( appBar: AppBar( title: Text(device.name), bottom: loading ? PreferredSize( preferredSize: const Size.fromHeight(4), child: LinearProgressIndicator(), ) : null, actions: [ IconButton(onPressed: moreActions, icon: Icon(Icons.adaptive.more)), ], ), body: SingleChildScrollView( child: Padding( padding: EdgeInsetsGeometry.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, spacing: 16, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(device.name, style: appTheme.textTheme.headlineLarge), Row( children: [ Text( appLocal.status, style: appTheme.textTheme.bodyLarge, ), Text( online ? appLocal.online : appLocal.offline, style: appTheme.textTheme.bodyLarge?.copyWith( color: online ? Colors.green : Colors.red, ), ), ], ), ], ), if (statusMessage.isNotEmpty) Text(statusMessage), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( spacing: 8, crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(Icons.info), Text( appLocal.deviceInfo, style: appTheme.textTheme.headlineMedium, ), ], ), Text(appLocal.lastKnownIP(device.ip ?? "Unknown")), Text("BSSID: ${device.bssid ?? "Unknown"}"), Text(appLocal.routerSsid(device.routerSsid)), ], ), Column( children: [ Row( spacing: 8, children: [ Icon(Icons.list), Text( appLocal.activityLogs, style: appTheme.textTheme.headlineMedium, ), ], ), 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), ], ); }, ), ], ), ], ), ), ), ); } 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), ), ], ), ); }, ), ], ), ); } }