392 lines
15 KiB
Dart
392 lines
15 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:anyhow/base.dart';
|
|
import 'package:bloc/bloc.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
|
import 'package:ot_viewer_app/owntracks_api.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
class SettingsState {
|
|
SettingsState({
|
|
this.url = '',
|
|
this.username = '',
|
|
this.password = '',
|
|
this.usersToDevices = const {},
|
|
this.activeDevices = const {},
|
|
});
|
|
|
|
final String url;
|
|
final String username;
|
|
final String password;
|
|
final Map<String, List<String>> usersToDevices;
|
|
final Set<(String, String)> activeDevices;
|
|
|
|
// Copy constructor
|
|
SettingsState.copy(SettingsState source)
|
|
: url = source.url,
|
|
username = source.username,
|
|
password = source.password,
|
|
usersToDevices = Map.from(source.usersToDevices)
|
|
.map((key, value) => MapEntry(key, List.from(value))),
|
|
activeDevices = Set.from(source.activeDevices);
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is SettingsState &&
|
|
runtimeType == other.runtimeType &&
|
|
url == other.url &&
|
|
username == other.username &&
|
|
password == other.password &&
|
|
usersToDevices == other.usersToDevices &&
|
|
activeDevices == other.activeDevices;
|
|
|
|
@override
|
|
int get hashCode =>
|
|
url.hashCode ^
|
|
username.hashCode ^
|
|
password.hashCode ^
|
|
usersToDevices.hashCode ^
|
|
activeDevices.hashCode;
|
|
|
|
@override
|
|
String toString() {
|
|
return 'SettingsState{url: $url, username: $username, password: $password, usersToDevices: $usersToDevices, activeDevices: $activeDevices}';
|
|
}
|
|
}
|
|
|
|
/*
|
|
Future<void> loadFromSharedPrefs() async {
|
|
final sp = await SharedPreferences.getInstance();
|
|
final url = sp.getString('url') ?? '';
|
|
final username = sp.getString('username') ?? '';
|
|
final password = sp.getString('password') ?? '';
|
|
|
|
// Decode the JSON string and then manually convert to the expected type
|
|
final usersToDevicesJson =
|
|
jsonDecode(sp.getString('usersToDevices') ?? '{}')
|
|
as Map<String, dynamic>;
|
|
final Map<String, List<String>> usersToDevices =
|
|
usersToDevicesJson.map((key, value) {
|
|
// Ensure the value is cast to a List<String>
|
|
final List<String> list = List<String>.from(value);
|
|
return MapEntry(key, list);
|
|
});
|
|
|
|
// Decode the JSON string for activeDevices. Adjust this part as needed based on your actual data structure
|
|
final activeDevicesJson =
|
|
jsonDecode(sp.getString('activeDevices') ?? '[]') as List;
|
|
final List<(String, String)> activeDevices = List.from(activeDevicesJson);
|
|
|
|
emit(SettingsState(
|
|
url: url,
|
|
username: username,
|
|
password: password,
|
|
usersToDevices: usersToDevices,
|
|
activeDevices: activeDevices,
|
|
));
|
|
}
|
|
|
|
|
|
*/
|
|
|
|
class SettingsCubit extends HydratedCubit<SettingsState> {
|
|
SettingsCubit() : super(SettingsState());
|
|
|
|
@override
|
|
void onChange(Change<SettingsState> change) {
|
|
// TODO: implement onChange
|
|
super.onChange(change);
|
|
print(change);
|
|
}
|
|
|
|
fetchDevices() async {
|
|
if (state.url.isEmpty) {
|
|
throw Exception("No URL provided, cannot fetch devices list!");
|
|
}
|
|
|
|
final api = OwntracksApi(
|
|
baseUrl: state.url,
|
|
username: state.username,
|
|
pass: state.password,
|
|
);
|
|
|
|
switch (await api.getDevices()) {
|
|
case Ok(:final ok):
|
|
final currentState = state;
|
|
|
|
// Emit new state
|
|
emit(SettingsState(
|
|
url: currentState.url,
|
|
username: currentState.username,
|
|
password: currentState.password,
|
|
usersToDevices: ok,
|
|
activeDevices: currentState.activeDevices,
|
|
));
|
|
case Err(:final err):
|
|
throw Exception("Fetching devices list failed: $err");
|
|
}
|
|
}
|
|
|
|
@override
|
|
SettingsState? fromJson(Map<String, dynamic> json) {
|
|
// print("fromjson $json");
|
|
final usersToDevicesMap =
|
|
(json['usersToDevices'] as Map<String, dynamic>?)?.map((key, value) {
|
|
final List<String> list = List<String>.from(value as List);
|
|
return MapEntry(key, list);
|
|
}) ??
|
|
{};
|
|
|
|
return SettingsState(
|
|
url: (json['url'] ?? '') as String,
|
|
username: (json['username'] ?? '') as String,
|
|
password: (json['password'] ?? '') as String,
|
|
usersToDevices: usersToDevicesMap,
|
|
activeDevices: Set<(String, String)>.from(
|
|
List<List<String>>.from(json['activeDevices']).map((e) {
|
|
if (e.length != 2) {
|
|
return null;
|
|
} else {
|
|
return (e[0], e[1]);
|
|
}
|
|
}).where((element) => element != null)));
|
|
}
|
|
|
|
@override
|
|
Map<String, dynamic>? toJson(SettingsState state) {
|
|
// print("tojson $state");
|
|
return {
|
|
'url': state.url,
|
|
'username': state.username,
|
|
'password': state.password,
|
|
'usersToDevices': state.usersToDevices,
|
|
'activeDevices': state.activeDevices.map((e) => [e.$1, e.$2]).toList(),
|
|
};
|
|
}
|
|
|
|
Future<void> updateSettings({
|
|
String? url,
|
|
String? username,
|
|
String? password,
|
|
Map<String, List<String>>? usersToDevices,
|
|
Set<(String, String)>? activeDevices,
|
|
}) async {
|
|
final currentState = state;
|
|
|
|
// Emit new state
|
|
emit(SettingsState(
|
|
url: url ?? currentState.url,
|
|
username: username ?? currentState.username,
|
|
password: password ?? currentState.password,
|
|
usersToDevices: usersToDevices ?? currentState.usersToDevices,
|
|
activeDevices: activeDevices ?? currentState.activeDevices,
|
|
));
|
|
}
|
|
}
|
|
|
|
class SettingsPage extends StatefulWidget {
|
|
const SettingsPage({super.key});
|
|
|
|
@override
|
|
createState() => _SettingsPageState();
|
|
}
|
|
|
|
class _SettingsPageState extends State<SettingsPage> {
|
|
final TextEditingController _urlController = TextEditingController();
|
|
final TextEditingController _usernameController = TextEditingController();
|
|
final TextEditingController _passwordController = TextEditingController();
|
|
|
|
@override
|
|
void dispose() {
|
|
_urlController.dispose();
|
|
_usernameController.dispose();
|
|
_passwordController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.only(left: 8, right: 8),
|
|
child: BlocConsumer<SettingsCubit, SettingsState>(
|
|
listener: (context, state) {
|
|
_urlController.text = state.url;
|
|
_usernameController.text = state.username;
|
|
_passwordController.text = state.password;
|
|
},
|
|
builder: (context, state) {
|
|
_urlController.text = state.url;
|
|
_usernameController.text = state.username;
|
|
_passwordController.text = state.password;
|
|
return SingleChildScrollView(
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(
|
|
color: Colors.redAccent.withOpacity(0.8), width: 2),
|
|
borderRadius: BorderRadius.circular(10)),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
// To make the column wrap its content
|
|
children: <Widget>[
|
|
TextField(
|
|
controller: _urlController,
|
|
decoration: const InputDecoration(
|
|
labelText:
|
|
'URL (e.g. https://your-owntracks.host.com)',
|
|
border: OutlineInputBorder(),
|
|
// Adds a border to the TextField
|
|
prefixIcon: Icon(
|
|
Icons.account_tree), // Adds an icon to the left
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
// Adds space between the two TextFields
|
|
TextField(
|
|
controller: _usernameController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Username',
|
|
border: OutlineInputBorder(),
|
|
// Adds a border to the TextField
|
|
prefixIcon:
|
|
Icon(Icons.person), // Adds an icon to the left
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
// Adds space between the two TextFields
|
|
TextField(
|
|
controller: _passwordController,
|
|
obscureText: true,
|
|
// Hides the text input (for passwords)
|
|
decoration: const InputDecoration(
|
|
labelText: 'Password',
|
|
border: OutlineInputBorder(),
|
|
// Adds a border to the TextField
|
|
prefixIcon:
|
|
Icon(Icons.lock), // Adds an icon to the left
|
|
),
|
|
),
|
|
const SizedBox(
|
|
height: 12,
|
|
),
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(Icons.warning,
|
|
color: Colors.red, size: 16),
|
|
// Small red exclamation mark icon
|
|
const SizedBox(width: 4),
|
|
// Space between icon and text
|
|
const Text(
|
|
'Password saved locally',
|
|
style: TextStyle(
|
|
color: Colors.red,
|
|
fontSize:
|
|
12, // Small font size for the warning text
|
|
),
|
|
),
|
|
const Spacer(),
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
print('saving settings...');
|
|
context.read<SettingsCubit>().updateSettings(
|
|
url: _urlController.text,
|
|
username: _usernameController.text,
|
|
password: _passwordController.text,
|
|
);
|
|
|
|
context.read<SettingsCubit>().fetchDevices();
|
|
},
|
|
icon: Icon(Icons.save),
|
|
label: Text('Save'))
|
|
],
|
|
),
|
|
]),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(
|
|
color: Colors.deepPurple.withOpacity(0.8), width: 2),
|
|
borderRadius: BorderRadius.circular(10)),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: List<Widget>.from([
|
|
Center(
|
|
child: ElevatedButton.icon(
|
|
onPressed: () =>
|
|
context.read<SettingsCubit>().fetchDevices(),
|
|
icon: const Icon(Icons.refresh),
|
|
label: const Text('Refresh Devices'),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
]) +
|
|
List<Widget>.from(
|
|
state.usersToDevices.entries.map((entry) {
|
|
return Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.person),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
entry.key,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 24,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const Divider(),
|
|
Container(
|
|
padding: const EdgeInsets.only(left: 8),
|
|
child: Column(children:
|
|
List<Widget>.from(entry.value.map((device) {
|
|
return Row(
|
|
children: [
|
|
Checkbox(
|
|
value: state.activeDevices
|
|
.contains((entry.key, device)),
|
|
onChanged: (newVal) {
|
|
var cubit =
|
|
context.read<SettingsCubit>();
|
|
Set<(String, String)> newSet =
|
|
Set.from(state.activeDevices);
|
|
if (newVal ?? false) {
|
|
newSet.add((entry.key, device));
|
|
} else {
|
|
newSet
|
|
.remove((entry.key, device));
|
|
}
|
|
cubit.updateSettings(
|
|
activeDevices: newSet);
|
|
}),
|
|
const SizedBox(width: 8),
|
|
Text("${entry.key}:$device"),
|
|
],
|
|
);
|
|
}))),
|
|
),
|
|
const SizedBox(height: 32),
|
|
],
|
|
);
|
|
}))),
|
|
)
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|