Skip to content

Commit 8cf7564

Browse files
authored
feat: select user on into screen (#312)
Rework developer settings and add "Change User" and "Log Out" actions. In the "Change User" action, a selection dialog displays all available users. When a user is changed, it is remembered as the default user, meaning it will be used as the login user at the next app start. Additionally, add an action to delete only the currently logged-in user database. Closes #310
1 parent 1fe2ed4 commit 8cf7564

25 files changed

+934
-223
lines changed

app/lib/app.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
7676
BlocProvider<LoadableUserCubit>(
7777
// loads the user on startup
7878
create: (context) =>
79-
LoadableUserCubit((_coreClient..loadUser()).userStream),
79+
LoadableUserCubit((_coreClient..loadDefaultUser()).userStream),
8080
lazy: false, // immediately try to load the user
8181
),
8282
],

app/lib/core/core_client.dart

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,24 @@ import 'package:logging/logging.dart';
1010
import 'package:path_provider/path_provider.dart';
1111
import 'package:prototype/core/core.dart';
1212
import 'package:prototype/util/platform.dart';
13+
import 'package:uuid/uuid_value.dart';
1314

1415
final _log = Logger('CoreClient');
1516

17+
Future<String> dbPath() async {
18+
final String path;
19+
20+
if (Platform.isAndroid || Platform.isIOS) {
21+
path = await getDatabaseDirectoryMobile();
22+
} else {
23+
final directory = await getApplicationDocumentsDirectory();
24+
path = directory.path;
25+
}
26+
27+
_log.info("Database path: $path");
28+
return path;
29+
}
30+
1631
class CoreClient {
1732
static final CoreClient _coreClient = CoreClient._internal();
1833

@@ -31,47 +46,42 @@ class CoreClient {
3146
Stream<User?> get userStream => _userController.stream;
3247

3348
User get user => _user!;
34-
set user(User user) {
49+
50+
set user(User? user) {
51+
_log.info("setting user: ${user?.userName}");
3552
_userController.add(user);
3653
_user = user;
3754
}
3855

39-
Future<String> dbPath() async {
40-
final String path;
41-
42-
if (Platform.isAndroid || Platform.isIOS) {
43-
path = await getDatabaseDirectoryMobile();
44-
} else {
45-
final directory = await getApplicationDocumentsDirectory();
46-
path = directory.path;
47-
}
48-
49-
_log.info("Database path: $path");
50-
return path;
56+
void logout() {
57+
user = null;
5158
}
5259

5360
// used in dev settings
5461
Future<void> deleteDatabase() async {
55-
await deleteDatabases(clientDbPath: await dbPath());
62+
await deleteDatabases(dbPath: await dbPath());
63+
_userController.add(null);
64+
_user = null;
65+
}
66+
67+
// used in dev settings
68+
Future<void> deleteUserDatabase() async {
69+
await deleteClientDatabase(
70+
dbPath: await dbPath(),
71+
userName: user.userName,
72+
clientId: user.clientId,
73+
);
5674
_userController.add(null);
5775
_user = null;
5876
}
5977

6078
// used in app initialization
61-
Future<bool> loadUser() async {
62-
try {
63-
user = await User.loadDefault(path: await dbPath());
64-
final userName = await user.userName;
65-
66-
_log.info("Loaded user: $userName");
67-
68-
return true;
69-
} catch (e) {
70-
_log.severe("Error when loading user: $e");
71-
_userController.add(null);
72-
_user = null;
73-
return false;
74-
}
79+
Future<void> loadDefaultUser() async {
80+
user = await User.loadDefault(path: await dbPath())
81+
.onError((error, stackTrace) {
82+
_log.severe("Error loading default user $error");
83+
return null;
84+
});
7585
}
7686

7787
// used in registration cubit
@@ -110,4 +120,15 @@ class CoreClient {
110120

111121
_log.info("User registered");
112122
}
123+
124+
Future<void> loadUser({
125+
required UiUserName userName,
126+
required UuidValue clientId,
127+
}) async {
128+
user = await User.load(
129+
dbPath: await dbPath(),
130+
userName: userName,
131+
clientId: clientId,
132+
);
133+
}
113134
}

app/lib/core/core_extension.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,18 @@ extension UiFlightPositionExtension on UiFlightPosition {
4444
};
4545
}
4646

47+
extension UiUserNameExtension on UiUserName {
48+
String displayName(String? displayName) =>
49+
displayName != null && displayName.isNotEmpty ? displayName : userName;
50+
}
51+
52+
extension DeviceTokenExtension on PlatformPushToken {
53+
String get token => switch (this) {
54+
PlatformPushToken_Apple(field0: final token) => token,
55+
PlatformPushToken_Google(field0: final token) => token,
56+
};
57+
}
58+
4759
extension ImageDataExtension on Uint8List {
4860
ImageData toImageData() =>
4961
ImageData(data: this, hash: ImageData.computeHash(this));
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// SPDX-FileCopyrightText: 2025 Phoenix R&D GmbH <[email protected]>
2+
//
3+
// SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
import 'package:flutter/material.dart';
6+
import 'package:prototype/core/core.dart';
7+
import 'package:prototype/theme/theme.dart';
8+
import 'package:prototype/user/user.dart';
9+
import 'package:prototype/widgets/widgets.dart';
10+
import 'package:provider/provider.dart';
11+
12+
class ChangeUserScreen extends StatefulWidget {
13+
const ChangeUserScreen({super.key});
14+
15+
@override
16+
State<ChangeUserScreen> createState() => _ChangeUserScreenState();
17+
}
18+
19+
class _ChangeUserScreenState extends State<ChangeUserScreen> {
20+
Future<List<UiClientRecord>>? _clientRecords;
21+
22+
@override
23+
void initState() {
24+
super.initState();
25+
loadClientRecords();
26+
}
27+
28+
void loadClientRecords() async {
29+
final clientRecords = User.loadClientRecords(dbPath: await dbPath());
30+
setState(() {
31+
_clientRecords = clientRecords;
32+
});
33+
}
34+
35+
@override
36+
Widget build(BuildContext context) {
37+
return ChangeUserScreenView(clientRecords: _clientRecords);
38+
}
39+
}
40+
41+
const _maxDesktopWidth = 800.0;
42+
43+
class ChangeUserScreenView extends StatelessWidget {
44+
const ChangeUserScreenView({
45+
this.clientRecords,
46+
super.key,
47+
});
48+
49+
final Future<List<UiClientRecord>>? clientRecords;
50+
51+
@override
52+
Widget build(BuildContext context) {
53+
return Scaffold(
54+
appBar: AppBar(
55+
title: const Text('Change User'),
56+
toolbarHeight: isPointer() ? 100 : null,
57+
leading: const AppBarBackButton(),
58+
),
59+
body: Center(
60+
child: Container(
61+
constraints: isPointer()
62+
? const BoxConstraints(maxWidth: _maxDesktopWidth)
63+
: null,
64+
child: _ClientRecords(clientRecords: clientRecords),
65+
),
66+
),
67+
);
68+
}
69+
}
70+
71+
class _ClientRecords extends StatelessWidget {
72+
const _ClientRecords({
73+
this.clientRecords,
74+
});
75+
76+
final Future<List<UiClientRecord>>? clientRecords;
77+
78+
@override
79+
Widget build(BuildContext context) {
80+
return FutureBuilder<List<UiClientRecord>>(
81+
future: clientRecords,
82+
builder: (context, snapshot) {
83+
if (snapshot.hasData) {
84+
return _ClientRecordsList(snapshot.data!);
85+
} else if (snapshot.hasError) {
86+
return Text(
87+
'Error loading contacts',
88+
);
89+
}
90+
return const CircularProgressIndicator();
91+
},
92+
);
93+
}
94+
}
95+
96+
class _ClientRecordsList extends StatelessWidget {
97+
const _ClientRecordsList(this.clientRecords);
98+
99+
final List<UiClientRecord> clientRecords;
100+
101+
@override
102+
Widget build(BuildContext context) {
103+
final user = context.select((LoadableUserCubit cubit) => cubit.state.user);
104+
105+
return Center(
106+
child: ListView(
107+
children: clientRecords.map((record) {
108+
final isCurrentUser = user?.userName ==
109+
"${record.userName.userName}@${record.userName.domain}";
110+
final currentUserSuffix = isCurrentUser ? " (current)" : "";
111+
112+
final textColor = isCurrentUser
113+
? Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.38)
114+
: null;
115+
116+
return ListTile(
117+
titleAlignment: ListTileTitleAlignment.top,
118+
titleTextStyle: Theme.of(context)
119+
.textTheme
120+
.bodyMedium
121+
?.copyWith(color: textColor, fontWeight: FontWeight.w600),
122+
subtitleTextStyle: Theme.of(context)
123+
.textTheme
124+
.bodySmall
125+
?.copyWith(color: textColor),
126+
leading: Transform.translate(
127+
offset: Offset(0, Spacings.xxs),
128+
child: UserAvatar(
129+
username: record.userName.userName,
130+
image: record.userProfile?.profilePicture,
131+
size: Spacings.xl,
132+
),
133+
),
134+
title: Text(
135+
record.userName.displayName(record.userProfile?.displayName) +
136+
currentUserSuffix,
137+
),
138+
subtitle: Text(
139+
"Domain: ${record.userName.domain}\n"
140+
"ID: ${record.clientId}\n"
141+
"Created: ${record.createdAt}\n"
142+
"Fully registered: ${record.isFinished ? "yes" : "no"}",
143+
),
144+
onTap: !isCurrentUser
145+
? () {
146+
final coreClient = context.read<CoreClient>();
147+
coreClient.logout();
148+
coreClient.loadUser(
149+
userName: record.userName,
150+
clientId: record.clientId,
151+
);
152+
}
153+
: null,
154+
);
155+
}).toList(),
156+
),
157+
);
158+
}
159+
}

app/lib/developer/developer.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
library;
77

88
export 'developer_settings_screen.dart';
9+
export 'change_user_screen.dart';

0 commit comments

Comments
 (0)