Initial commit

This commit is contained in:
2025-11-30 15:42:05 +07:00
commit 99344a82e6
139 changed files with 7801 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
import 'dart:convert';
import 'dart:io';
import 'package:json_annotation/json_annotation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
part 'appconfig.g.dart';
@JsonSerializable()
class AppConfig {
List<Device> devices = [];
AppConfig._constructor();
static AppConfig _instance = AppConfig._constructor();
factory AppConfig({Map<String, dynamic>? json}) {
if (json != null) {
_instance = _$AppConfigFromJson(json);
}
return _instance;
}
Map<String, dynamic> toJson() => _$AppConfigToJson(this);
/// Saves configuration to file.
///
/// WARN: This method assumes the config file already exists.
Future<void> save() async {
final appDocsDir = await getApplicationDocumentsDirectory();
var configFile = File(p.join(appDocsDir.path, "app.json"));
await configFile.writeAsString(jsonEncode(toJson()));
}
}
@JsonSerializable()
class Device {
String name;
String routerSsid;
String routerBssid;
String networkPassword;
String? ip;
String? bssid;
Device({
required this.name,
required this.routerSsid,
required this.routerBssid,
this.networkPassword = "",
});
factory Device.fromJson(Map<String, dynamic> json) => _$DeviceFromJson(json);
Map<String, dynamic> toJson() => _$DeviceToJson(this);
}
+35
View File
@@ -0,0 +1,35 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'appconfig.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AppConfig _$AppConfigFromJson(Map<String, dynamic> json) => AppConfig()
..devices = (json['devices'] as List<dynamic>)
.map((e) => Device.fromJson(e as Map<String, dynamic>))
.toList();
Map<String, dynamic> _$AppConfigToJson(AppConfig instance) => <String, dynamic>{
'devices': instance.devices,
};
Device _$DeviceFromJson(Map<String, dynamic> json) =>
Device(
name: json['name'] as String,
routerSsid: json['routerSsid'] as String,
routerBssid: json['routerBssid'] as String,
networkPassword: json['networkPassword'] as String? ?? "",
)
..ip = json['ip'] as String?
..bssid = json['bssid'] as String?;
Map<String, dynamic> _$DeviceToJson(Device instance) => <String, dynamic>{
'name': instance.name,
'routerSsid': instance.routerSsid,
'routerBssid': instance.routerBssid,
'networkPassword': instance.networkPassword,
'ip': instance.ip,
'bssid': instance.bssid,
};
+97
View File
@@ -0,0 +1,97 @@
import 'package:esptouch_flutter/esptouch_flutter.dart';
import 'package:flutter/material.dart';
import 'package:liteauthconfig/l10n/app_localizations.dart';
import '../appconfig.dart';
class ESPTouchDialog extends StatefulWidget {
final String name;
final String ssid;
final String bssid;
final String password;
final int? entryIndex;
final bool addNewEntry;
const ESPTouchDialog({
super.key,
required this.name,
required this.ssid,
required this.bssid,
required this.password,
this.addNewEntry = false,
this.entryIndex
});
@override
State<StatefulWidget> createState() => _ESPTouchDialog();
}
class _ESPTouchDialog extends State<ESPTouchDialog> {
@override
void initState() {
execute();
super.initState();
}
void execute() {
final task = ESPTouchTask(
ssid: widget.ssid,
bssid: widget.bssid,
password: widget.password,
packet: ESPTouchPacket.multicast
);
final touchStream = task.execute();
final sub = touchStream.listen((data) async {
final dev = Device(
name: widget.name,
routerSsid: widget.ssid,
routerBssid: widget.bssid,
networkPassword: widget.password,
);
dev.ip = data.ip;
dev.bssid = data.bssid;
var conf = AppConfig();
if (widget.addNewEntry) {
conf.devices.add(dev);
} else {
conf.devices[widget.entryIndex!] = dev;
}
await conf.save();
if (context.mounted) {
// ignore: use_build_context_synchronously
Navigator.pop(context);
}
});
Future.delayed(Duration(minutes: 1), () {
sub.cancel();
if (context.mounted) {
// ignore: use_build_context_synchronously
Navigator.pop(context);
}
});
}
@override
Widget build(BuildContext context) {
final appLocal = AppLocalizations.of(context)!;
return AlertDialog(
title: Text(appLocal.networkInitHeadline),
content: Column(
mainAxisSize: MainAxisSize.min,
spacing: 16,
children: [
Text(appLocal.networkInitDetails),
LinearProgressIndicator(),
],
),
actions: [],
);
}
}
+49
View File
@@ -0,0 +1,49 @@
{
"deviceList": "Device List",
"noDevices": "It seems that you have no devices! You can register one with the big plus button.",
"setupNewDevice": "Setup new device",
"addExistingDevice": "Add existing device",
"registerDevice": "Register Device",
"deviceName": "Name",
"networkName": "Network Name (SSID)",
"routerMacAddress": "Router MAC Address (BSSID)",
"networkPassword": "Network Password",
"networkLimitation": "Only 2.4 GHz networks allowed!",
"networkLimitationDesc": "We suspect that your network may be 5 GHz based on the name. Please note that liteauth can only connect to 2.4 GHz networks.",
"networkInitHeadline": "Network Initialization",
"networkInitDetails": "Broadcasting network information to your liteauth device, This will be done in a minute or two. Please also make sure your device is powered on.",
"networkInfoHeadline": "Network Information",
"networkInfoPrompt": "Do you wish to automatically fill in the current network information? The app will ask to access your fine location if you continue.",
"continueText": "Continue",
"cancel": "Cancel",
"deviceInfo": "Device Information",
"delete": "Delete",
"deletion": "Deletion",
"deviceDeleteConfirm": "Are you sure you want to delete this device forever?",
"reconfigureNetwork": "Reconfigure Network",
"status": "Status: ",
"online": "Online",
"offline": "Offline",
"refresh": "Refresh",
"lastKnownIP": "Last Known IP: {ip}",
"@lastKnownIP": {
"placeholders": {
"ip": {
"type": "String",
"example": "192.168.1.10"
}
}
},
"routerSsid": "Router SSID: {ssid}",
"@routerSsid": {
"placeholders": {
"ssid": {
"type": "String",
"example": "ABC_WiFi"
}
}
}
}
+302
View File
@@ -0,0 +1,302 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'app_localizations_en.dart';
import 'app_localizations_th.dart';
// ignore_for_file: type=lint
/// Callers can lookup localized strings with an instance of AppLocalizations
/// returned by `AppLocalizations.of(context)`.
///
/// Applications need to include `AppLocalizations.delegate()` in their app's
/// `localizationDelegates` list, and the locales they support in the app's
/// `supportedLocales` list. For example:
///
/// ```dart
/// import 'l10n/app_localizations.dart';
///
/// return MaterialApp(
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
/// supportedLocales: AppLocalizations.supportedLocales,
/// home: MyApplicationHome(),
/// );
/// ```
///
/// ## Update pubspec.yaml
///
/// Please make sure to update your pubspec.yaml to include the following
/// packages:
///
/// ```yaml
/// dependencies:
/// # Internationalization support.
/// flutter_localizations:
/// sdk: flutter
/// intl: any # Use the pinned version from flutter_localizations
///
/// # Rest of dependencies
/// ```
///
/// ## iOS Applications
///
/// iOS applications define key application metadata, including supported
/// locales, in an Info.plist file that is built into the application bundle.
/// To configure the locales supported by your app, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects Runner folder.
///
/// Next, select the Information Property List item, select Add Item from the
/// Editor menu, then select Localizations from the pop-up menu.
///
/// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale
/// you wish to add from the pop-up menu in the Value field. This list should
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
/// property.
abstract class AppLocalizations {
AppLocalizations(String locale)
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
static AppLocalizations? of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
///
/// Returns a list of localizations delegates containing this delegate along with
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
/// and GlobalWidgetsLocalizations.delegate.
///
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
Locale('en'),
Locale('th'),
];
/// No description provided for @deviceList.
///
/// In en, this message translates to:
/// **'Device List'**
String get deviceList;
/// No description provided for @noDevices.
///
/// In en, this message translates to:
/// **'It seems that you have no devices! You can register one with the big plus button.'**
String get noDevices;
/// No description provided for @setupNewDevice.
///
/// In en, this message translates to:
/// **'Setup new device'**
String get setupNewDevice;
/// No description provided for @addExistingDevice.
///
/// In en, this message translates to:
/// **'Add existing device'**
String get addExistingDevice;
/// No description provided for @registerDevice.
///
/// In en, this message translates to:
/// **'Register Device'**
String get registerDevice;
/// No description provided for @deviceName.
///
/// In en, this message translates to:
/// **'Name'**
String get deviceName;
/// No description provided for @networkName.
///
/// In en, this message translates to:
/// **'Network Name (SSID)'**
String get networkName;
/// No description provided for @routerMacAddress.
///
/// In en, this message translates to:
/// **'Router MAC Address (BSSID)'**
String get routerMacAddress;
/// No description provided for @networkPassword.
///
/// In en, this message translates to:
/// **'Network Password'**
String get networkPassword;
/// No description provided for @networkLimitation.
///
/// In en, this message translates to:
/// **'Only 2.4 GHz networks allowed!'**
String get networkLimitation;
/// No description provided for @networkLimitationDesc.
///
/// In en, this message translates to:
/// **'We suspect that your network may be 5 GHz based on the name. Please note that liteauth can only connect to 2.4 GHz networks.'**
String get networkLimitationDesc;
/// No description provided for @networkInitHeadline.
///
/// In en, this message translates to:
/// **'Network Initialization'**
String get networkInitHeadline;
/// No description provided for @networkInitDetails.
///
/// In en, this message translates to:
/// **'Broadcasting network information to your liteauth device, This will be done in a minute or two. Please also make sure your device is powered on.'**
String get networkInitDetails;
/// No description provided for @networkInfoHeadline.
///
/// In en, this message translates to:
/// **'Network Information'**
String get networkInfoHeadline;
/// No description provided for @networkInfoPrompt.
///
/// In en, this message translates to:
/// **'Do you wish to automatically fill in the current network information? The app will ask to access your fine location if you continue.'**
String get networkInfoPrompt;
/// No description provided for @continueText.
///
/// In en, this message translates to:
/// **'Continue'**
String get continueText;
/// No description provided for @cancel.
///
/// In en, this message translates to:
/// **'Cancel'**
String get cancel;
/// No description provided for @deviceInfo.
///
/// In en, this message translates to:
/// **'Device Information'**
String get deviceInfo;
/// No description provided for @delete.
///
/// In en, this message translates to:
/// **'Delete'**
String get delete;
/// No description provided for @deletion.
///
/// In en, this message translates to:
/// **'Deletion'**
String get deletion;
/// No description provided for @deviceDeleteConfirm.
///
/// In en, this message translates to:
/// **'Are you sure you want to delete this device forever?'**
String get deviceDeleteConfirm;
/// No description provided for @reconfigureNetwork.
///
/// In en, this message translates to:
/// **'Reconfigure Network'**
String get reconfigureNetwork;
/// No description provided for @status.
///
/// In en, this message translates to:
/// **'Status: '**
String get status;
/// No description provided for @online.
///
/// In en, this message translates to:
/// **'Online'**
String get online;
/// No description provided for @offline.
///
/// In en, this message translates to:
/// **'Offline'**
String get offline;
/// No description provided for @refresh.
///
/// In en, this message translates to:
/// **'Refresh'**
String get refresh;
/// No description provided for @lastKnownIP.
///
/// In en, this message translates to:
/// **'Last Known IP: {ip}'**
String lastKnownIP(String ip);
/// No description provided for @routerSsid.
///
/// In en, this message translates to:
/// **'Router SSID: {ssid}'**
String routerSsid(String ssid);
}
class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) {
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) =>
<String>['en', 'th'].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'en':
return AppLocalizationsEn();
case 'th':
return AppLocalizationsTh();
}
throw FlutterError(
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.',
);
}
+103
View File
@@ -0,0 +1,103 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get deviceList => 'Device List';
@override
String get noDevices =>
'It seems that you have no devices! You can register one with the big plus button.';
@override
String get setupNewDevice => 'Setup new device';
@override
String get addExistingDevice => 'Add existing device';
@override
String get registerDevice => 'Register Device';
@override
String get deviceName => 'Name';
@override
String get networkName => 'Network Name (SSID)';
@override
String get routerMacAddress => 'Router MAC Address (BSSID)';
@override
String get networkPassword => 'Network Password';
@override
String get networkLimitation => 'Only 2.4 GHz networks allowed!';
@override
String get networkLimitationDesc =>
'We suspect that your network may be 5 GHz based on the name. Please note that liteauth can only connect to 2.4 GHz networks.';
@override
String get networkInitHeadline => 'Network Initialization';
@override
String get networkInitDetails =>
'Broadcasting network information to your liteauth device, This will be done in a minute or two. Please also make sure your device is powered on.';
@override
String get networkInfoHeadline => 'Network Information';
@override
String get networkInfoPrompt =>
'Do you wish to automatically fill in the current network information? The app will ask to access your fine location if you continue.';
@override
String get continueText => 'Continue';
@override
String get cancel => 'Cancel';
@override
String get deviceInfo => 'Device Information';
@override
String get delete => 'Delete';
@override
String get deletion => 'Deletion';
@override
String get deviceDeleteConfirm =>
'Are you sure you want to delete this device forever?';
@override
String get reconfigureNetwork => 'Reconfigure Network';
@override
String get status => 'Status: ';
@override
String get online => 'Online';
@override
String get offline => 'Offline';
@override
String get refresh => 'Refresh';
@override
String lastKnownIP(String ip) {
return 'Last Known IP: $ip';
}
@override
String routerSsid(String ssid) {
return 'Router SSID: $ssid';
}
}
+102
View File
@@ -0,0 +1,102 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Thai (`th`).
class AppLocalizationsTh extends AppLocalizations {
AppLocalizationsTh([String locale = 'th']) : super(locale);
@override
String get deviceList => 'รายการอุปกรณ์';
@override
String get noDevices =>
'ดูเหมือนว่าคุณจะไม่มีอุปกรณ์ที่ลงทะเบียนไว้นะ! คุณสามารถลงทะเบียนอุปกรณ์ใหม่ได้ที่ปุ่มบวกด้านล่าง';
@override
String get setupNewDevice => 'ตั้งค่าอุปกรณ์ใหม่';
@override
String get addExistingDevice => 'เพิ่มอุปกรณ์ที่ตั้งค่าแล้ว';
@override
String get registerDevice => 'ลงทะเบียนอุปกรณ์';
@override
String get deviceName => 'ชื่อ';
@override
String get networkName => 'ชื่อเครือข่าย (SSID)';
@override
String get routerMacAddress => 'MAC Address ของเราเตอร์ (BSSID)';
@override
String get networkPassword => 'รหัสผ่านเครือข่าย';
@override
String get networkLimitation => 'อนุญาตแค่เครือข่าย 2.4 GHz เท่านั้น!';
@override
String get networkLimitationDesc =>
'เราสงสัยว่าเครือข่ายคุณอาจเป็นเครือข่าย 5 GHz จากชื่อ โปรดจำไว้ว่า liteauth สามารถเชื่อมต่อได้กับเครือข่าย 2.4 GHz เท่านั้น';
@override
String get networkInitHeadline => 'การตั้งค่าเครือข่าย';
@override
String get networkInitDetails =>
'กำลังเผยแพร่ข้อมูลเครือข่ายไปยังอุปกรณ์ liteauth ของคุณ กระบวนการนี้อาจใช้เวลาหนึ่งถึงสองนาที และโปรดตรวจสอบให้แน่ใจว่าคุณเปิดอุปกรณ์ของคุณแล้ว';
@override
String get networkInfoHeadline => 'ข้อมูลเครือข่าย';
@override
String get networkInfoPrompt =>
'คุณต้องการที่จะใส่ข้อมูลเครือข่ายปัจจุบันโดยอัตโนมัติหรือไม่ แอพลิเคชั่นจะขอความอนุญาตในการเข้าถึงข้อมูลตำแหน่งแบบละเอียดหากคุณดำเนินการต่อ';
@override
String get continueText => 'ดำเนินการต่อ';
@override
String get cancel => 'ยกเลิก';
@override
String get deviceInfo => 'ข้อมูลอุปกรณ์';
@override
String get delete => 'ลบ';
@override
String get deletion => 'การลบ';
@override
String get deviceDeleteConfirm => 'คุณแน่ใจหรือไม่ที่จะลบอุปกรณ์นี้อย่างถาวร';
@override
String get reconfigureNetwork => 'ตั้งค่าเครือข่ายใหม่';
@override
String get status => 'สถานะ: ';
@override
String get online => 'ออนไลน์';
@override
String get offline => 'ออฟไลน์';
@override
String get refresh => 'รีเฟรช';
@override
String lastKnownIP(String ip) {
return 'ที่อยู่ IP ที่ทราบล่าสุด: $ip';
}
@override
String routerSsid(String ssid) {
return 'SSID เราเตอร์: $ssid';
}
}
+33
View File
@@ -0,0 +1,33 @@
{
"deviceList": "รายการอุปกรณ์",
"noDevices": "ดูเหมือนว่าคุณจะไม่มีอุปกรณ์ที่ลงทะเบียนไว้นะ! คุณสามารถลงทะเบียนอุปกรณ์ใหม่ได้ที่ปุ่มบวกด้านล่าง",
"setupNewDevice": "ตั้งค่าอุปกรณ์ใหม่",
"addExistingDevice": "เพิ่มอุปกรณ์ที่ตั้งค่าแล้ว",
"registerDevice": "ลงทะเบียนอุปกรณ์",
"deviceName": "ชื่อ",
"networkName": "ชื่อเครือข่าย (SSID)",
"routerMacAddress": "MAC Address ของเราเตอร์ (BSSID)",
"networkPassword": "รหัสผ่านเครือข่าย",
"networkLimitation": "อนุญาตแค่เครือข่าย 2.4 GHz เท่านั้น!",
"networkLimitationDesc": "เราสงสัยว่าเครือข่ายคุณอาจเป็นเครือข่าย 5 GHz จากชื่อ โปรดจำไว้ว่า liteauth สามารถเชื่อมต่อได้กับเครือข่าย 2.4 GHz เท่านั้น",
"networkInitHeadline": "การตั้งค่าเครือข่าย",
"networkInitDetails": "กำลังเผยแพร่ข้อมูลเครือข่ายไปยังอุปกรณ์ liteauth ของคุณ กระบวนการนี้อาจใช้เวลาหนึ่งถึงสองนาที และโปรดตรวจสอบให้แน่ใจว่าคุณเปิดอุปกรณ์ของคุณแล้ว",
"networkInfoHeadline": "ข้อมูลเครือข่าย",
"networkInfoPrompt": "คุณต้องการที่จะใส่ข้อมูลเครือข่ายปัจจุบันโดยอัตโนมัติหรือไม่ แอพลิเคชั่นจะขอความอนุญาตในการเข้าถึงข้อมูลตำแหน่งแบบละเอียดหากคุณดำเนินการต่อ",
"continueText": "ดำเนินการต่อ",
"cancel": "ยกเลิก",
"deviceInfo": "ข้อมูลอุปกรณ์",
"delete": "ลบ",
"deletion": "การลบ",
"deviceDeleteConfirm": "คุณแน่ใจหรือไม่ที่จะลบอุปกรณ์นี้อย่างถาวร",
"reconfigureNetwork": "ตั้งค่าเครือข่ายใหม่",
"status": "สถานะ: ",
"online": "ออนไลน์",
"offline": "ออฟไลน์",
"refresh": "รีเฟรช",
"lastKnownIP": "ที่อยู่ IP ที่ทราบล่าสุด: {ip}",
"routerSsid": "SSID เราเตอร์: {ssid}"
}
+153
View File
@@ -0,0 +1,153 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:liteauthconfig/appconfig.dart';
import 'package:liteauthconfig/pages/devicelist.dart';
import 'package:liteauthconfig/l10n/app_localizations.dart';
import 'package:path_provider/path_provider.dart';
import 'package:system_theme/system_theme.dart';
import 'package:path/path.dart' as p;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
SystemTheme.fallbackColor = const Color(0xFF4C1D95);
await SystemTheme.accentColor.load();
final appDocsDir = await getApplicationDocumentsDirectory();
var configFile = File(p.join(appDocsDir.path, "app.json"));
if (await configFile.exists()) {
AppConfig(json: jsonDecode(await configFile.readAsString()));
} else {
await configFile.create();
await configFile.writeAsString(jsonEncode(AppConfig().toJson()));
}
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
var darkColors = ColorScheme.fromSeed(
seedColor: SystemTheme.accentColor.accent,
);
var darkTheme = ThemeData.dark();
return MaterialApp(
title: 'liteauthconfig',
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: Locale("th"),
theme: ThemeData.light().copyWith(
colorScheme: ColorScheme.fromSeed(
seedColor: SystemTheme.accentColor.accent,
),
),
darkTheme: darkTheme.copyWith(
colorScheme: darkColors,
dialogTheme: DialogThemeData(
backgroundColor: darkColors.inverseSurface,
),
cardTheme: CardThemeData(color: darkColors.primary),
listTileTheme: ListTileThemeData(
textColor: Colors.white,
iconColor: Colors.white,
),
),
home: DeviceListPage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
+187
View File
@@ -0,0 +1,187 @@
import 'package:flutter/material.dart';
import 'package:liteauthconfig/appconfig.dart';
import 'package:liteauthconfig/l10n/app_localizations.dart';
import 'package:liteauthconfig/pages/reconfigure_network.dart';
import 'package:liteauthconfig/utils/network.dart';
class DeviceInfoPage extends StatefulWidget {
final int deviceIndex;
const DeviceInfoPage({super.key, required this.deviceIndex});
@override
State<StatefulWidget> createState() => _DeviceInfoPageState();
}
class _DeviceInfoPageState extends State<DeviceInfoPage> {
late Device device;
String statusMessage = "";
bool online = false;
bool loading = false;
@override
void initState() {
super.initState();
refreshDevice();
checkStatus();
}
void refreshDevice() {
device = AppConfig().devices[widget.deviceIndex];
}
void checkStatus() async {
setState(() {
loading = true;
});
var client = await createEspHttpClient();
try {
var resp = await client.get(Uri.parse("https://liteauth.local"));
setState(() {
online = resp.statusCode >= 200 && resp.statusCode < 300;
});
} catch (e) {
statusMessage = e.toString();
}
setState(() {
loading = false;
});
}
@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: [
MenuAnchor(
builder: (context, controller, child) {
return IconButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
icon: const Icon(Icons.more_vert),
);
},
menuChildren: [
MenuItemButton(
leadingIcon: const Icon(Icons.delete),
child: Text(appLocal.delete),
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
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),
),
],
),
);
},
),
MenuItemButton(
leadingIcon: const Icon(Icons.network_wifi),
child: Text(appLocal.reconfigureNetwork),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ReconfigureNetworkPage(widget.deviceIndex),
),
).then((_) => refreshDevice());
},
),
MenuItemButton(
leadingIcon: const Icon(Icons.refresh),
onPressed: checkStatus,
child: Text(appLocal.refresh),
),
],
),
],
),
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)),
],
),
],
),
),
),
);
}
}
+102
View File
@@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
import 'package:liteauthconfig/appconfig.dart';
import 'package:liteauthconfig/l10n/app_localizations.dart';
import 'package:liteauthconfig/pages/deviceinfo.dart';
import 'package:liteauthconfig/pages/registerpage.dart';
class DeviceListPage extends StatefulWidget {
const DeviceListPage({super.key});
@override
State<StatefulWidget> createState() => _DeviceListPageState();
}
class _DeviceListPageState extends State<DeviceListPage> {
List<Device> devices = [];
@override
void initState() {
super.initState();
refreshList();
}
void refreshList() {
setState(() {
devices = AppConfig().devices;
});
}
@override
Widget build(BuildContext context) {
var appLocal = AppLocalizations.of(context)!;
var appTheme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: Text(appLocal.deviceList)),
body: Visibility(
visible: devices.isEmpty,
replacement: ListView.builder(
itemBuilder: (context, index) {
final dev = devices[index];
return ListTile(
title: Text(dev.name),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DeviceInfoPage(deviceIndex: index),
),
).then((_) => refreshList());
},
);
},
itemCount: devices.length,
shrinkWrap: true,
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
appLocal.noDevices,
textAlign: TextAlign.center,
style: appTheme.textTheme.bodyLarge,
),
),
),
floatingActionButton: SpeedDial(
overlayColor: Colors.black,
spacing: 8,
children: [
SpeedDialChild(
child: const Icon(Icons.settings),
label: appLocal.setupNewDevice,
shape: CircleBorder(),
labelStyle: appTheme.textTheme.bodyMedium?.copyWith(
color: Colors.black,
),
onTap: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => RegisterPage(true)))
.then((_) => refreshList());
},
),
SpeedDialChild(
child: const Icon(Icons.add_box_outlined),
label: appLocal.addExistingDevice,
shape: CircleBorder(),
labelStyle: appTheme.textTheme.bodyMedium?.copyWith(
color: Colors.black,
),
onTap: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => RegisterPage(false)))
.then((_) => refreshList());
}
),
],
child: const Icon(Icons.add),
),
);
}
}
+195
View File
@@ -0,0 +1,195 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:liteauthconfig/appconfig.dart';
import 'package:liteauthconfig/dialogs/esptouchdialog.dart';
import 'package:liteauthconfig/l10n/app_localizations.dart';
import 'package:network_info_plus/network_info_plus.dart';
import 'package:permission_handler/permission_handler.dart';
// ignore_for_file: use_build_context_synchronously
class ReconfigureNetworkPage extends StatefulWidget {
final int deviceIndex;
const ReconfigureNetworkPage(this.deviceIndex, {super.key});
@override
State<StatefulWidget> createState() => _ReconfigureNetworkPageState();
}
class _ReconfigureNetworkPageState extends State<ReconfigureNetworkPage> {
TextEditingController ssidController = TextEditingController();
TextEditingController bssidController = TextEditingController();
TextEditingController passwordController = TextEditingController();
bool potentially5GHz = false;
bool obscurePassword = true;
@override
void initState() {
super.initState();
askForNetworkInfo();
}
void askForNetworkInfo() async {
await Future.delayed(Duration(milliseconds: 250));
if (!context.mounted) {
return;
}
var appLocal = AppLocalizations.of(context)!;
var appTheme = Theme.of(context);
if (await Permission.location.isDenied) {
showDialog(
context: context,
builder: (context) => AlertDialog.adaptive(
backgroundColor: appTheme.cardColor,
title: Text(appLocal.networkInfoHeadline),
content: Text(appLocal.networkInfoPrompt),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(appLocal.cancel),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
if (await Permission.location.request().isGranted) {
fetchNetworkInfo();
}
},
child: Text(appLocal.continueText),
),
],
),
);
} else if (await Permission.location.isGranted) {
fetchNetworkInfo();
}
}
void fetchNetworkInfo() async {
final dev = AppConfig().devices[widget.deviceIndex];
var netInfo = NetworkInfo();
var name = await netInfo.getWifiName() ?? dev.routerSsid;
if (Platform.isAndroid) {
name = name.substring(1, name.length - 1);
}
ssidController.text = name;
var bssid = await netInfo.getWifiBSSID();
if (bssid == dev.routerBssid) {
passwordController.text = dev.networkPassword;
}
bssidController.text = bssid ?? dev.routerBssid;
if (name.contains("5G")) {
setState(() {
potentially5GHz = true;
});
}
}
void submit() async {
var dev = AppConfig().devices[widget.deviceIndex];
await showDialog(
context: context,
builder: (context) => ESPTouchDialog(
name: dev.name,
ssid: ssidController.text,
bssid: bssidController.text,
password: passwordController.text,
entryIndex: widget.deviceIndex,
),
);
if (context.mounted) {
Navigator.pop(context);
}
}
@override
Widget build(BuildContext context) {
final appLocal = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(appLocal.reconfigureNetwork),
),
floatingActionButton: FloatingActionButton(
onPressed: submit,
child: const Icon(Icons.check),
),
body: Padding(
padding: EdgeInsetsGeometry.all(16),
child: Column(
spacing: 16,
children: [
if (potentially5GHz)
Card(
child: Column(
children: [
ListTile(
leading: const Icon(Icons.warning),
title: Text(appLocal.networkLimitation),
subtitle: Text(appLocal.networkLimitationDesc),
),
],
),
),
TextField(
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: appLocal.networkName,
),
controller: ssidController,
onChanged: (name) {
setState(() {
potentially5GHz = name.contains("5G");
});
},
),
TextField(
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: appLocal.routerMacAddress,
),
controller: bssidController,
),
TextField(
obscureText: obscurePassword,
controller: passwordController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: appLocal.networkPassword,
suffixIcon: IconButton(
onPressed: () {
setState(() {
obscurePassword = !obscurePassword;
});
},
icon: Icon(
obscurePassword
? Icons.remove_red_eye
: Icons.remove_red_eye_outlined,
),
),
),
),
],
),
),
);
}
}
+219
View File
@@ -0,0 +1,219 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:liteauthconfig/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';
// ignore_for_file: use_build_context_synchronously
class RegisterPage extends StatefulWidget {
final bool setup;
const RegisterPage(this.setup, {super.key});
@override
State<StatefulWidget> createState() => _RegisterPageState();
}
class _RegisterPageState extends State<RegisterPage> {
TextEditingController nameController = TextEditingController();
TextEditingController ssidController = TextEditingController();
TextEditingController bssidController = TextEditingController();
TextEditingController passwordController = TextEditingController();
bool potentially5GHz = false;
bool obscurePassword = true;
@override
void initState() {
super.initState();
askForNetworkInfo();
}
void askForNetworkInfo() async {
await Future.delayed(Duration(milliseconds: 250));
if (!context.mounted) {
return;
}
var appLocal = AppLocalizations.of(context)!;
var appTheme = Theme.of(context);
if (await Permission.location.isDenied) {
showDialog(
context: context,
builder: (context) => AlertDialog.adaptive(
backgroundColor: appTheme.cardColor,
title: Text(appLocal.networkInfoHeadline),
content: Text(appLocal.networkInfoPrompt),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(appLocal.cancel),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
if (await Permission.location.request().isGranted) {
fetchNetworkInfo();
}
},
child: Text(appLocal.continueText),
),
],
),
);
} else if (await Permission.location.isGranted) {
fetchNetworkInfo();
}
}
void fetchNetworkInfo() async {
var netInfo = NetworkInfo();
var name = await netInfo.getWifiName() ?? "";
if (Platform.isAndroid) {
name = name.substring(1, name.length - 1);
}
ssidController.text = name;
bssidController.text = await netInfo.getWifiBSSID() ?? "";
if (name.contains("5G")) {
setState(() {
potentially5GHz = true;
});
}
}
void submit() async {
if (widget.setup) {
await showDialog(
context: context,
barrierDismissible: false,
builder: (context) => ESPTouchDialog(
name: nameController.text,
ssid: ssidController.text,
bssid: bssidController.text,
password: passwordController.text,
addNewEntry: true,
),
);
} else {
final dev = Device(
name: nameController.text,
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) {
Navigator.pop(context);
}
}
@override
Widget build(BuildContext context) {
var appLocal = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(appLocal.registerDevice)),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
spacing: 16,
children: [
if (potentially5GHz)
Card(
child: Column(
children: [
ListTile(
leading: const Icon(Icons.warning),
title: Text(appLocal.networkLimitation),
subtitle: Text(appLocal.networkLimitationDesc),
),
],
),
),
TextField(
controller: nameController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: appLocal.deviceName,
),
),
TextField(
controller: ssidController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: appLocal.networkName,
),
onChanged: (name) {
setState(() {
potentially5GHz = name.contains("5G");
});
},
),
TextField(
controller: bssidController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: appLocal.routerMacAddress,
),
),
TextField(
obscureText: obscurePassword,
controller: passwordController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: appLocal.networkPassword,
suffixIcon: IconButton(
onPressed: () {
setState(() {
obscurePassword = !obscurePassword;
});
},
icon: Icon(
obscurePassword
? Icons.remove_red_eye
: Icons.remove_red_eye_outlined,
),
),
),
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: submit,
child: const Icon(Icons.check),
),
);
}
}
+17
View File
@@ -0,0 +1,17 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
Future<http.Client> createEspHttpClient() async {
final certData = await rootBundle.load("assets/certificates/rootCA.crt");
final certBytes = certData.buffer.asUint8List();
SecurityContext securityContext = SecurityContext.defaultContext;
securityContext.setTrustedCertificatesBytes(certBytes);
HttpClient client = HttpClient(context: securityContext);
return IOClient(client);
}