Skip to content

Commit 8b3560e

Browse files
authored
Add accountspage (#426)
* Add accountspage * Add delete user * remove pubspec.lock
1 parent 8987768 commit 8b3560e

File tree

7 files changed

+440
-11
lines changed

7 files changed

+440
-11
lines changed

lib/main.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'package:settings/services/settings_service.dart';
1616
import 'package:ubuntu_service/ubuntu_service.dart';
1717
import 'package:udisks/udisks.dart';
1818
import 'package:upower/upower.dart';
19+
import 'package:xdg_accounts/xdg_accounts.dart';
1920
import 'package:yaru_widgets/yaru_widgets.dart';
2021

2122
void main() async {
@@ -77,6 +78,10 @@ void main() async {
7778
DisplayService.new,
7879
dispose: (s) => s.dispose(),
7980
);
81+
registerService<XdgAccounts>(
82+
XdgAccounts.new,
83+
dispose: (s) => s.dispose(),
84+
);
8085

8186
runApp(const UbuntuSettingsApp());
8287
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import 'dart:async';
2+
3+
import 'package:safe_change_notifier/safe_change_notifier.dart';
4+
import 'package:xdg_accounts/xdg_accounts.dart';
5+
6+
class AccountsModel extends SafeChangeNotifier {
7+
AccountsModel(this._xdgAccounts);
8+
9+
final XdgAccounts _xdgAccounts;
10+
11+
StreamSubscription<bool>? _usersChangedSub;
12+
List<XdgUser>? get users => _xdgAccounts.xdgUsers;
13+
14+
Future<void> addUser({
15+
required String name,
16+
required String fullname,
17+
required int accountType,
18+
required String password,
19+
required String passwordHint,
20+
}) async {
21+
final path = await _xdgAccounts.createUser(
22+
name: name,
23+
fullname: fullname,
24+
accountType: accountType,
25+
);
26+
final user = _xdgAccounts.findUserByPath(path);
27+
if (user != null) {
28+
await user.setLocked(false);
29+
await user.setPasswordMode(0);
30+
await user.setPassword(password, passwordHint);
31+
}
32+
}
33+
34+
Future<void> deleteUser({
35+
required int id,
36+
required String name,
37+
required bool removeFiles,
38+
}) async =>
39+
await _xdgAccounts.deleteUser(
40+
id: id,
41+
name: name,
42+
removeFiles: removeFiles,
43+
);
44+
45+
Future<void> init() async {
46+
await _xdgAccounts.init();
47+
_usersChangedSub = _xdgAccounts.usersChanged.listen((event) {
48+
notifyListeners();
49+
});
50+
notifyListeners();
51+
}
52+
53+
@override
54+
void dispose() {
55+
super.dispose();
56+
_usersChangedSub?.cancel();
57+
}
58+
}
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:provider/provider.dart';
3+
import 'package:settings/constants.dart';
4+
import 'package:settings/l10n/l10n.dart';
5+
import 'package:settings/view/common/yaru_switch_row.dart';
6+
import 'package:settings/view/pages/accounts/accounts_model.dart';
7+
import 'package:settings/view/pages/accounts/user_model.dart';
8+
import 'package:settings/view/pages/privacy/house_keeping_page.dart';
9+
import 'package:settings/view/pages/settings_page.dart';
10+
import 'package:ubuntu_service/ubuntu_service.dart';
11+
import 'package:xdg_accounts/xdg_accounts.dart';
12+
import 'package:yaru_icons/yaru_icons.dart';
13+
import 'package:yaru_widgets/yaru_widgets.dart';
14+
15+
class AccountsPage extends StatelessWidget {
16+
const AccountsPage({super.key});
17+
18+
static Widget create(BuildContext context) =>
19+
ChangeNotifierProvider<AccountsModel>(
20+
create: (context) => AccountsModel(getService<XdgAccounts>())..init(),
21+
child: const AccountsPage(),
22+
);
23+
24+
static Widget createTitle(BuildContext context) =>
25+
Text(context.l10n.usersPageTitle);
26+
27+
static bool searchMatches(String value, BuildContext context) => value
28+
.isNotEmpty
29+
? context.l10n.usersPageTitle.toLowerCase().contains(value.toLowerCase())
30+
: false;
31+
32+
@override
33+
Widget build(BuildContext context) {
34+
final model = context.watch<AccountsModel>();
35+
return SettingsPage(
36+
children: [
37+
Center(
38+
child: SizedBox(
39+
width: kDefaultWidth,
40+
child: Column(
41+
children: [
42+
YaruTile(
43+
title: const Text('Add user'),
44+
leading: YaruIconButton(
45+
icon: const Icon(
46+
YaruIcons.plus,
47+
),
48+
onPressed: () => showDialog(
49+
context: context,
50+
builder: (context) =>
51+
ChangeNotifierProvider<AccountsModel>.value(
52+
value: model,
53+
child: const _AddUserDialog(),
54+
),
55+
),
56+
),
57+
),
58+
for (final user in model.users ?? <XdgUser>[])
59+
_UserTile.create(
60+
context: context,
61+
user: user,
62+
deleteUser: model.deleteUser,
63+
init: () async {
64+
await Future.delayed(const Duration(seconds: 1));
65+
},
66+
)
67+
],
68+
),
69+
),
70+
)
71+
],
72+
);
73+
}
74+
}
75+
76+
class _AddUserDialog extends StatefulWidget {
77+
const _AddUserDialog();
78+
79+
@override
80+
State<_AddUserDialog> createState() => _AddUserDialogState();
81+
}
82+
83+
class _AddUserDialogState extends State<_AddUserDialog> {
84+
late TextEditingController _usernameController;
85+
late TextEditingController _fullNameController;
86+
late TextEditingController _passwordController;
87+
late TextEditingController _passwordHintController;
88+
XdgAccountType _accountType = XdgAccountType.user;
89+
90+
@override
91+
void initState() {
92+
super.initState();
93+
_usernameController = TextEditingController();
94+
_passwordController = TextEditingController();
95+
_fullNameController = TextEditingController();
96+
_passwordHintController = TextEditingController();
97+
}
98+
99+
@override
100+
void dispose() {
101+
_usernameController.dispose();
102+
_passwordController.dispose();
103+
_fullNameController.dispose();
104+
_passwordHintController.dispose();
105+
super.dispose();
106+
}
107+
108+
@override
109+
Widget build(BuildContext context) {
110+
final model = context.watch<AccountsModel>();
111+
112+
return AlertDialog(
113+
title: const YaruDialogTitleBar(
114+
title: Text('Add User'),
115+
),
116+
titlePadding: EdgeInsets.zero,
117+
contentPadding: const EdgeInsets.all(kYaruPagePadding),
118+
content: Column(
119+
mainAxisSize: MainAxisSize.min,
120+
children: [
121+
SizedBox(
122+
width: 350,
123+
child: YaruSwitchRow(
124+
value: _accountType == XdgAccountType.admin,
125+
onChanged: (value) => setState(() {
126+
_accountType =
127+
value ? XdgAccountType.admin : XdgAccountType.user;
128+
}),
129+
trailingWidget: const Text('Admin'), // TODO: localize
130+
),
131+
),
132+
const SizedBox(
133+
height: kYaruPagePadding,
134+
),
135+
TextField(
136+
controller: _usernameController,
137+
decoration: const InputDecoration(labelText: 'username'),
138+
),
139+
const SizedBox(
140+
height: kYaruPagePadding,
141+
),
142+
TextField(
143+
controller: _fullNameController,
144+
decoration: const InputDecoration(labelText: 'full name'),
145+
),
146+
const SizedBox(
147+
height: kYaruPagePadding,
148+
),
149+
TextField(
150+
controller: _passwordController,
151+
obscureText: true,
152+
decoration: const InputDecoration(labelText: 'password'),
153+
),
154+
const SizedBox(
155+
height: kYaruPagePadding,
156+
),
157+
TextField(
158+
controller: _passwordHintController,
159+
obscureText: true,
160+
decoration: const InputDecoration(labelText: 'Password hint'),
161+
)
162+
],
163+
),
164+
actions: [
165+
ElevatedButton(
166+
onPressed: () => model
167+
.addUser(
168+
name: _usernameController.text,
169+
fullname: _fullNameController.text,
170+
accountType: _accountType.index,
171+
password: _passwordController.text,
172+
passwordHint: _passwordHintController.text,
173+
)
174+
.then((_) {
175+
model.init().then((value) => Navigator.pop(context));
176+
}),
177+
child: Text(context.l10n.confirm),
178+
)
179+
],
180+
);
181+
}
182+
}
183+
184+
class _UserTile extends StatelessWidget {
185+
const _UserTile({required this.deleteUser, required this.init});
186+
187+
final Future<void> Function({
188+
required int id,
189+
required String name,
190+
required bool removeFiles,
191+
}) deleteUser;
192+
final Future<void> Function() init;
193+
194+
static Widget create({
195+
required BuildContext context,
196+
required XdgUser user,
197+
required Future<void> Function({
198+
required int id,
199+
required String name,
200+
required bool removeFiles,
201+
}) deleteUser,
202+
required final Future<void> Function() init,
203+
}) {
204+
return ChangeNotifierProvider<UserModel>(
205+
create: (context) => UserModel(user)..init(),
206+
child: _UserTile(
207+
deleteUser: deleteUser,
208+
init: init,
209+
),
210+
);
211+
}
212+
213+
@override
214+
Widget build(BuildContext context) {
215+
final model = context.watch<UserModel>();
216+
final theme = Theme.of(context);
217+
218+
return YaruTile(
219+
leading: model.iconFile != null
220+
? CircleAvatar(
221+
radius: 20,
222+
backgroundImage: FileImage(model.iconFile!),
223+
)
224+
: CircleAvatar(
225+
radius: 20,
226+
backgroundColor: theme.colorScheme.inverseSurface,
227+
child: Center(
228+
child: Text(
229+
model.userName?.substring(0, 1) ?? '',
230+
style: TextStyle(
231+
fontSize: 20,
232+
color: theme.colorScheme.onInverseSurface,
233+
),
234+
),
235+
),
236+
),
237+
title: Text(model.userName ?? ''),
238+
subtitle: Text(
239+
model.accountType?.name ?? '',
240+
),
241+
trailing: Row(
242+
children: [
243+
YaruIconButton(
244+
icon: const Icon(YaruIcons.pen),
245+
onPressed: () => showDialog(
246+
context: context,
247+
builder: (context) => ChangeNotifierProvider<UserModel>.value(
248+
value: model,
249+
child: const _EditUserDialog(),
250+
),
251+
),
252+
),
253+
if (model.id != null || model.userName == null)
254+
YaruIconButton(
255+
icon: const Icon(YaruIcons.trash),
256+
onPressed: () => showDialog(
257+
context: context,
258+
builder: (context) => ConfirmationDialog(
259+
iconData: YaruIcons.trash,
260+
onConfirm: () => deleteUser(
261+
id: model.id!,
262+
name: model.userName!,
263+
removeFiles: true,
264+
).then((_) {
265+
init().then((value) => Navigator.pop(context));
266+
}),
267+
),
268+
),
269+
),
270+
],
271+
),
272+
);
273+
}
274+
}
275+
276+
class _EditUserDialog extends StatefulWidget {
277+
const _EditUserDialog();
278+
279+
@override
280+
State<_EditUserDialog> createState() => _EditUserDialogState();
281+
}
282+
283+
class _EditUserDialogState extends State<_EditUserDialog> {
284+
late TextEditingController userNameController;
285+
286+
@override
287+
void initState() {
288+
super.initState();
289+
userNameController = TextEditingController();
290+
}
291+
292+
@override
293+
void dispose() {
294+
userNameController.dispose();
295+
super.dispose();
296+
}
297+
298+
@override
299+
Widget build(BuildContext context) {
300+
final model = context.watch<UserModel>();
301+
userNameController.text = model.userName ?? '';
302+
return SimpleDialog(
303+
titlePadding: EdgeInsets.zero,
304+
contentPadding: const EdgeInsets.all(kYaruPagePadding),
305+
title: YaruTitleBar(
306+
title: Text(model.userName ?? ''),
307+
),
308+
children: [
309+
TextField(
310+
controller: userNameController,
311+
onSubmitted: (value) => model.userName = value,
312+
)
313+
],
314+
);
315+
}
316+
}

0 commit comments

Comments
 (0)