owntracks-viewer/lib/settings_page.dart

392 lines
15 KiB
Dart
Raw Normal View History

2024-03-12 20:41:04 +00:00
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),
],
);
}))),
)
],
),
);
},
),
);
}
}