Add user-profile popup
This commit is contained in:
parent
cb87c08be6
commit
1e336688b7
7 changed files with 282 additions and 42 deletions
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
||||||
|
|
||||||
import 'package:contacts_plus_plus/clients/api_client.dart';
|
import 'package:contacts_plus_plus/clients/api_client.dart';
|
||||||
import 'package:contacts_plus_plus/models/friend.dart';
|
import 'package:contacts_plus_plus/models/friend.dart';
|
||||||
|
import 'package:contacts_plus_plus/models/personal_profile.dart';
|
||||||
import 'package:contacts_plus_plus/models/user.dart';
|
import 'package:contacts_plus_plus/models/user.dart';
|
||||||
import 'package:contacts_plus_plus/models/user_profile.dart';
|
import 'package:contacts_plus_plus/models/user_profile.dart';
|
||||||
|
|
||||||
|
@ -20,6 +21,13 @@ class UserApi {
|
||||||
return User.fromMap(data);
|
return User.fromMap(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<PersonalProfile> getPersonalProfile(ApiClient client) async {
|
||||||
|
final response = await client.get("/users/${client.userId}");
|
||||||
|
ApiClient.checkResponse(response);
|
||||||
|
final data = jsonDecode(response.body);
|
||||||
|
return PersonalProfile.fromMap(data);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<void> addUserAsFriend(ApiClient client, {required User user}) async {
|
static Future<void> addUserAsFriend(ApiClient client, {required User user}) async {
|
||||||
final friend = Friend(
|
final friend = Friend(
|
||||||
id: user.id,
|
id: user.id,
|
||||||
|
|
|
@ -15,6 +15,12 @@ import 'package:workmanager/workmanager.dart';
|
||||||
enum EventType {
|
enum EventType {
|
||||||
unknown,
|
unknown,
|
||||||
message,
|
message,
|
||||||
|
unknown1,
|
||||||
|
unknown2,
|
||||||
|
unknown3,
|
||||||
|
unknown4,
|
||||||
|
keepAlive,
|
||||||
|
error;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum EventTarget {
|
enum EventTarget {
|
||||||
|
@ -44,6 +50,7 @@ class MessagingClient {
|
||||||
final Logger _logger = Logger("NeosHub");
|
final Logger _logger = Logger("NeosHub");
|
||||||
final Workmanager _workmanager = Workmanager();
|
final Workmanager _workmanager = Workmanager();
|
||||||
final NotificationClient _notificationClient;
|
final NotificationClient _notificationClient;
|
||||||
|
int _attempts = 0;
|
||||||
Function? _unreadsUpdateListener;
|
Function? _unreadsUpdateListener;
|
||||||
WebSocket? _wsChannel;
|
WebSocket? _wsChannel;
|
||||||
bool _isConnecting = false;
|
bool _isConnecting = false;
|
||||||
|
@ -138,9 +145,9 @@ class MessagingClient {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDisconnected(error) {
|
void _onDisconnected(error) async {
|
||||||
_logger.warning("Neos Hub connection died with error '$error', reconnecting...");
|
_logger.warning("Neos Hub connection died with error '$error', reconnecting...");
|
||||||
start();
|
await start();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> start() async {
|
Future<void> start() async {
|
||||||
|
@ -161,7 +168,6 @@ class MessagingClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<WebSocket> _tryConnect() async {
|
Future<WebSocket> _tryConnect() async {
|
||||||
int attempts = 0;
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
final http.Response response;
|
final http.Response response;
|
||||||
|
@ -181,13 +187,15 @@ class MessagingClient {
|
||||||
if (url == null || wsToken == null) {
|
if (url == null || wsToken == null) {
|
||||||
throw "Invalid response from server.";
|
throw "Invalid response from server.";
|
||||||
}
|
}
|
||||||
return await WebSocket.connect("$url&access_token=$wsToken");
|
final ws = await WebSocket.connect("$url&access_token=$wsToken");
|
||||||
|
_attempts = 0;
|
||||||
|
return ws;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
final timeout = _reconnectTimeoutsSeconds[attempts.clamp(0, _reconnectTimeoutsSeconds.length - 1)];
|
final timeout = _reconnectTimeoutsSeconds[_attempts.clamp(0, _reconnectTimeoutsSeconds.length - 1)];
|
||||||
_logger.severe(e);
|
_logger.severe(e);
|
||||||
_logger.severe("Retrying in $timeout seconds");
|
_logger.severe("Retrying in $timeout seconds");
|
||||||
await Future.delayed(Duration(seconds: timeout));
|
await Future.delayed(Duration(seconds: timeout));
|
||||||
attempts++;
|
_attempts++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -208,12 +216,24 @@ class MessagingClient {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch (EventType.values[rawType]) {
|
switch (EventType.values[rawType]) {
|
||||||
|
case EventType.unknown1:
|
||||||
|
case EventType.unknown2:
|
||||||
|
case EventType.unknown3:
|
||||||
|
case EventType.unknown4:
|
||||||
case EventType.unknown:
|
case EventType.unknown:
|
||||||
_logger.info("[Hub]: Unknown event received: $rawType: $body");
|
_logger.info("Received unknown event: $rawType: $body");
|
||||||
break;
|
break;
|
||||||
case EventType.message:
|
case EventType.message:
|
||||||
|
_logger.info("Received message-event.");
|
||||||
_handleMessageEvent(body);
|
_handleMessageEvent(body);
|
||||||
break;
|
break;
|
||||||
|
case EventType.keepAlive:
|
||||||
|
_logger.info("Received keep-alive.");
|
||||||
|
break;
|
||||||
|
case EventType.error:
|
||||||
|
_logger.severe("Received error-event: ${body["error"]}");
|
||||||
|
// Should we trigger a manual reconnect here or just let the remote service close the connection?
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_phoenix/flutter_phoenix.dart';
|
import 'package:flutter_phoenix/flutter_phoenix.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:workmanager/workmanager.dart';
|
import 'package:workmanager/workmanager.dart';
|
||||||
import 'clients/api_client.dart';
|
|
||||||
import 'models/authentication_data.dart';
|
import 'models/authentication_data.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
|
@ -22,7 +21,7 @@ void main() async {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.root.onRecord.listen((event) => log(event.message, name: event.loggerName));
|
Logger.root.onRecord.listen((event) => log(event.message, name: event.loggerName, time: event.time));
|
||||||
final settingsClient = SettingsClient();
|
final settingsClient = SettingsClient();
|
||||||
await settingsClient.loadSettings();
|
await settingsClient.loadSettings();
|
||||||
runApp(Phoenix(child: ContactsPlusPlus(settingsClient: settingsClient,)));
|
runApp(Phoenix(child: ContactsPlusPlus(settingsClient: settingsClient,)));
|
||||||
|
|
63
lib/models/personal_profile.dart
Normal file
63
lib/models/personal_profile.dart
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:contacts_plus_plus/models/user_profile.dart';
|
||||||
|
|
||||||
|
class PersonalProfile {
|
||||||
|
final String id;
|
||||||
|
final String username;
|
||||||
|
final String email;
|
||||||
|
final DateTime? publicBanExpiration;
|
||||||
|
final String? publicBanType;
|
||||||
|
final List<StorageQuotas> storageQuotas;
|
||||||
|
final Map<String, int> quotaBytesSource;
|
||||||
|
final int usedBytes;
|
||||||
|
final bool twoFactor;
|
||||||
|
final bool isPatreonSupporter;
|
||||||
|
final UserProfile userProfile;
|
||||||
|
|
||||||
|
PersonalProfile({
|
||||||
|
required this.id, required this.username, required this.email, required this.publicBanExpiration,
|
||||||
|
required this.publicBanType, required this.storageQuotas, required this.quotaBytesSource, required this.usedBytes,
|
||||||
|
required this.twoFactor, required this.isPatreonSupporter, required this.userProfile,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory PersonalProfile.fromMap(Map map) {
|
||||||
|
final banExp = map["publicBanExpiration"];
|
||||||
|
return PersonalProfile(
|
||||||
|
id: map["id"],
|
||||||
|
username: map["username"],
|
||||||
|
email: map["email"],
|
||||||
|
publicBanExpiration: banExp == null ? null : DateTime.parse(banExp),
|
||||||
|
publicBanType: map["publicBanType"],
|
||||||
|
storageQuotas: (map["storageQuotas"] as List).map((e) => StorageQuotas.fromMap(e)).toList(),
|
||||||
|
quotaBytesSource: (map["quotaBytesSources"] as Map).map((key, value) => MapEntry(key, value as int)),
|
||||||
|
usedBytes: map["usedBytes"],
|
||||||
|
twoFactor: map["2fa_login"] ?? false,
|
||||||
|
isPatreonSupporter: map["patreonData"]?["isPatreonSupporter"] ?? false,
|
||||||
|
userProfile: UserProfile.fromMap(map["profile"]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int get maxBytes => (quotaBytesSource.values.maxOrNull ?? 0) + storageQuotas.map((e) => e.bytes).reduce((value, element) => value + element);
|
||||||
|
}
|
||||||
|
|
||||||
|
class StorageQuotas {
|
||||||
|
final String id;
|
||||||
|
final int bytes;
|
||||||
|
final DateTime addedOn;
|
||||||
|
final DateTime expiresOn;
|
||||||
|
final String giftedByUserId;
|
||||||
|
|
||||||
|
StorageQuotas({required this.id, required this.bytes, required this.addedOn, required this.expiresOn,
|
||||||
|
required this.giftedByUserId});
|
||||||
|
|
||||||
|
factory StorageQuotas.fromMap(Map map) {
|
||||||
|
return StorageQuotas(
|
||||||
|
id: map["id"],
|
||||||
|
bytes: map["bytes"],
|
||||||
|
addedOn: DateTime.parse(map["addedOn"]),
|
||||||
|
expiresOn: DateTime.parse(map["expiresOn"]),
|
||||||
|
giftedByUserId: map["giftedByUserId"] ?? "",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,16 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:contacts_plus_plus/apis/user_api.dart';
|
||||||
import 'package:contacts_plus_plus/client_holder.dart';
|
import 'package:contacts_plus_plus/client_holder.dart';
|
||||||
import 'package:contacts_plus_plus/apis/friend_api.dart';
|
import 'package:contacts_plus_plus/apis/friend_api.dart';
|
||||||
import 'package:contacts_plus_plus/apis/message_api.dart';
|
import 'package:contacts_plus_plus/apis/message_api.dart';
|
||||||
import 'package:contacts_plus_plus/models/friend.dart';
|
import 'package:contacts_plus_plus/models/friend.dart';
|
||||||
import 'package:contacts_plus_plus/models/message.dart';
|
import 'package:contacts_plus_plus/models/message.dart';
|
||||||
|
import 'package:contacts_plus_plus/models/personal_profile.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/default_error_widget.dart';
|
import 'package:contacts_plus_plus/widgets/default_error_widget.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/expanding_input_fab.dart';
|
import 'package:contacts_plus_plus/widgets/expanding_input_fab.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/friend_list_tile.dart';
|
import 'package:contacts_plus_plus/widgets/friend_list_tile.dart';
|
||||||
|
import 'package:contacts_plus_plus/widgets/my_profile_dialog.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/settings_page.dart';
|
import 'package:contacts_plus_plus/widgets/settings_page.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/user_search.dart';
|
import 'package:contacts_plus_plus/widgets/user_search.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -32,6 +35,7 @@ class _FriendsListState extends State<FriendsList> {
|
||||||
static const Duration _autoRefreshDuration = Duration(seconds: 90);
|
static const Duration _autoRefreshDuration = Duration(seconds: 90);
|
||||||
static const Duration _refreshTimeoutDuration = Duration(seconds: 30);
|
static const Duration _refreshTimeoutDuration = Duration(seconds: 30);
|
||||||
Future<List<Friend>>? _friendsFuture;
|
Future<List<Friend>>? _friendsFuture;
|
||||||
|
Future<PersonalProfile>? _userProfileFuture;
|
||||||
ClientHolder? _clientHolder;
|
ClientHolder? _clientHolder;
|
||||||
Timer? _autoRefresh;
|
Timer? _autoRefresh;
|
||||||
Timer? _refreshTimeout;
|
Timer? _refreshTimeout;
|
||||||
|
@ -59,6 +63,7 @@ class _FriendsListState extends State<FriendsList> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_refreshFriendsList();
|
_refreshFriendsList();
|
||||||
|
_userProfileFuture = UserApi.getPersonalProfile(_clientHolder!.apiClient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,32 +106,72 @@ class _FriendsListState extends State<FriendsList> {
|
||||||
onSelected: (MenuItemDefinition itemDef) async {
|
onSelected: (MenuItemDefinition itemDef) async {
|
||||||
await itemDef.onTap();
|
await itemDef.onTap();
|
||||||
},
|
},
|
||||||
itemBuilder: (BuildContext context) => [
|
itemBuilder: (BuildContext context) =>
|
||||||
MenuItemDefinition(name: "Settings", icon: Icons.settings, onTap: () async {
|
[
|
||||||
_autoRefresh?.cancel();
|
MenuItemDefinition(
|
||||||
await Navigator.of(context).push(MaterialPageRoute(builder: (context) => const SettingsPage()));
|
name: "Settings",
|
||||||
_autoRefresh = Timer(_autoRefreshDuration, () => setState(() => _refreshFriendsList()));
|
icon: Icons.settings,
|
||||||
}),
|
onTap: () async {
|
||||||
MenuItemDefinition(name: "Find Users", icon: Icons.person_add, onTap: () async {
|
_autoRefresh?.cancel();
|
||||||
bool changed = false;
|
await Navigator.of(context).push(MaterialPageRoute(builder: (context) => const SettingsPage()));
|
||||||
_autoRefresh?.cancel();
|
|
||||||
await Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) =>
|
|
||||||
UserSearch(
|
|
||||||
onFriendsChanged: () => changed = true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (changed) {
|
|
||||||
_refreshTimeout?.cancel();
|
|
||||||
setState(() {
|
|
||||||
_refreshFriendsList();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
_autoRefresh = Timer(_autoRefreshDuration, () => setState(() => _refreshFriendsList()));
|
_autoRefresh = Timer(_autoRefreshDuration, () => setState(() => _refreshFriendsList()));
|
||||||
}
|
},
|
||||||
}),
|
),
|
||||||
|
MenuItemDefinition(
|
||||||
|
name: "Find Users",
|
||||||
|
icon: Icons.person_add,
|
||||||
|
onTap: () async {
|
||||||
|
bool changed = false;
|
||||||
|
_autoRefresh?.cancel();
|
||||||
|
await Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
UserSearch(
|
||||||
|
onFriendsChanged: () => changed = true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (changed) {
|
||||||
|
_refreshTimeout?.cancel();
|
||||||
|
setState(() {
|
||||||
|
_refreshFriendsList();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_autoRefresh = Timer(_autoRefreshDuration, () => setState(() => _refreshFriendsList()));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MenuItemDefinition(
|
||||||
|
name: "My Profile",
|
||||||
|
icon: Icons.person,
|
||||||
|
onTap: () async {
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return FutureBuilder(
|
||||||
|
future: _userProfileFuture,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
final profile = snapshot.data as PersonalProfile;
|
||||||
|
return MyProfileDialog(profile: profile);
|
||||||
|
} else if (snapshot.hasError) {
|
||||||
|
return DefaultErrorWidget(
|
||||||
|
title: "Failed to load personal profile.",
|
||||||
|
onRetry: () {
|
||||||
|
setState(() {
|
||||||
|
_userProfileFuture = UserApi.getPersonalProfile(ClientHolder.of(context).apiClient);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const Center(child: CircularProgressIndicator(),);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
].map((item) =>
|
].map((item) =>
|
||||||
PopupMenuItem<MenuItemDefinition>(
|
PopupMenuItem<MenuItemDefinition>(
|
||||||
value: item,
|
value: item,
|
||||||
|
@ -156,7 +201,8 @@ class _FriendsListState extends State<FriendsList> {
|
||||||
if (snapshot.hasData) {
|
if (snapshot.hasData) {
|
||||||
var friends = (snapshot.data as List<Friend>);
|
var friends = (snapshot.data as List<Friend>);
|
||||||
if (_searchFilter.isNotEmpty) {
|
if (_searchFilter.isNotEmpty) {
|
||||||
friends = friends.where((element) => element.username.toLowerCase().contains(_searchFilter.toLowerCase())).toList();
|
friends = friends.where((element) =>
|
||||||
|
element.username.toLowerCase().contains(_searchFilter.toLowerCase())).toList();
|
||||||
friends.sort((a, b) => a.username.length.compareTo(b.username.length));
|
friends.sort((a, b) => a.username.length.compareTo(b.username.length));
|
||||||
}
|
}
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
|
|
|
@ -2,14 +2,16 @@ import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class GenericAvatar extends StatelessWidget {
|
class GenericAvatar extends StatelessWidget {
|
||||||
const GenericAvatar({this.imageUri="", super.key, this.placeholderIcon=Icons.person});
|
const GenericAvatar({this.imageUri="", super.key, this.placeholderIcon=Icons.person, this.radius});
|
||||||
|
|
||||||
final String imageUri;
|
final String imageUri;
|
||||||
final IconData placeholderIcon;
|
final IconData placeholderIcon;
|
||||||
|
final double? radius;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return imageUri.isEmpty ? CircleAvatar(
|
return imageUri.isEmpty ? CircleAvatar(
|
||||||
|
radius: radius,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
child: Icon(placeholderIcon),
|
child: Icon(placeholderIcon),
|
||||||
) : CachedNetworkImage(
|
) : CachedNetworkImage(
|
||||||
|
@ -17,18 +19,22 @@ class GenericAvatar extends StatelessWidget {
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
foregroundImage: imageProvider,
|
foregroundImage: imageProvider,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
|
radius: radius,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
imageUrl: imageUri,
|
imageUrl: imageUri,
|
||||||
placeholder: (context, url) {
|
placeholder: (context, url) {
|
||||||
return const CircleAvatar(
|
return CircleAvatar(
|
||||||
backgroundColor: Colors.white54,
|
backgroundColor: Colors.white54,
|
||||||
child: Padding(
|
radius: radius,
|
||||||
padding: EdgeInsets.all(8.0),
|
child: const Padding(
|
||||||
child: CircularProgressIndicator(color: Colors.black38, strokeWidth: 2),
|
padding: EdgeInsets.all(8.0),
|
||||||
));
|
child: CircularProgressIndicator(color: Colors.black38, strokeWidth: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
errorWidget: (context, error, what) => CircleAvatar(
|
errorWidget: (context, error, what) => CircleAvatar(
|
||||||
|
radius: radius,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
child: Icon(placeholderIcon),
|
child: Icon(placeholderIcon),
|
||||||
),
|
),
|
||||||
|
|
98
lib/widgets/my_profile_dialog.dart
Normal file
98
lib/widgets/my_profile_dialog.dart
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import 'package:contacts_plus_plus/auxiliary.dart';
|
||||||
|
import 'package:contacts_plus_plus/models/personal_profile.dart';
|
||||||
|
import 'package:contacts_plus_plus/widgets/generic_avatar.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
class MyProfileDialog extends StatelessWidget {
|
||||||
|
const MyProfileDialog({required this.profile, super.key});
|
||||||
|
|
||||||
|
final PersonalProfile profile;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final tt = Theme.of(context).textTheme;
|
||||||
|
DateFormat dateFormat = DateFormat.yMd();
|
||||||
|
return Dialog(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(profile.username, style: tt.titleLarge),
|
||||||
|
Text(profile.email, style: tt.labelMedium?.copyWith(color: Colors.white54),)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
GenericAvatar(imageUri: Aux.neosDbToHttp(profile.userProfile.iconUrl), radius: 24,)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16,),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [Text("User ID: ", style: tt.labelLarge,), Text(profile.id)],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [Text("2FA: ", style: tt.labelLarge,), Text(profile.twoFactor ? "Enabled" : "Disabled")],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [Text("Patreon Supporter: ", style: tt.labelLarge,), Text(profile.isPatreonSupporter ? "Yes" : "No")],
|
||||||
|
),
|
||||||
|
if (profile.publicBanExpiration?.isAfter(DateTime.now()) ?? false)
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [Text("Ban Expiration: ", style: tt.labelLarge,),
|
||||||
|
Text(dateFormat.format(profile.publicBanExpiration!))],
|
||||||
|
),
|
||||||
|
StorageIndicator(usedBytes: profile.usedBytes, maxBytes: profile.maxBytes,),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StorageIndicator extends StatelessWidget {
|
||||||
|
const StorageIndicator({required this.usedBytes, required this.maxBytes, super.key});
|
||||||
|
|
||||||
|
final int usedBytes;
|
||||||
|
final int maxBytes;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final value = usedBytes/maxBytes;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text("Storage:", style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
Text("${(usedBytes * 1e-9).toStringAsFixed(2)}/${(maxBytes * 1e-9).toStringAsFixed(2)} GiB"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8,),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
minHeight: 12,
|
||||||
|
color: value > 0.95 ? Theme.of(context).colorScheme.error : null,
|
||||||
|
value: value,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue