diff --git a/lib/apis/friend_api.dart b/lib/apis/friend_api.dart index 9bdce6d..9e8a331 100644 --- a/lib/apis/friend_api.dart +++ b/lib/apis/friend_api.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:contacts_plus_plus/clients/api_client.dart'; -import 'package:contacts_plus_plus/models/friend.dart'; +import 'package:contacts_plus_plus/models/users/friend.dart'; class FriendApi { static Future> getFriendsList(ApiClient client, {DateTime? lastStatusUpdate}) async { diff --git a/lib/apis/session_api.dart b/lib/apis/session_api.dart new file mode 100644 index 0000000..fab8d1c --- /dev/null +++ b/lib/apis/session_api.dart @@ -0,0 +1,13 @@ +import 'dart:convert'; + +import 'package:contacts_plus_plus/clients/api_client.dart'; +import 'package:contacts_plus_plus/models/session.dart'; + +class SessionApi { + static Future getSession(ApiClient client, {required String sessionId}) async { + final response = await client.get("/sessions/$sessionId"); + client.checkResponse(response); + final body = jsonDecode(response.body); + return Session.fromMap(body); + } +} \ No newline at end of file diff --git a/lib/apis/user_api.dart b/lib/apis/user_api.dart index e85d5a5..c43bbff 100644 --- a/lib/apis/user_api.dart +++ b/lib/apis/user_api.dart @@ -1,10 +1,12 @@ import 'dart:convert'; import 'package:contacts_plus_plus/clients/api_client.dart'; -import 'package:contacts_plus_plus/models/friend.dart'; +import 'package:contacts_plus_plus/models/users/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_profile.dart'; +import 'package:contacts_plus_plus/models/users/user.dart'; +import 'package:contacts_plus_plus/models/users/user_profile.dart'; +import 'package:contacts_plus_plus/models/users/friend_status.dart'; +import 'package:contacts_plus_plus/models/users/user_status.dart'; import 'package:package_info_plus/package_info_plus.dart'; class UserApi { diff --git a/lib/clients/messaging_client.dart b/lib/clients/messaging_client.dart index 680eedb..0203e76 100644 --- a/lib/clients/messaging_client.dart +++ b/lib/clients/messaging_client.dart @@ -11,7 +11,7 @@ import 'package:contacts_plus_plus/apis/friend_api.dart'; import 'package:contacts_plus_plus/apis/message_api.dart'; import 'package:contacts_plus_plus/apis/user_api.dart'; import 'package:contacts_plus_plus/clients/notification_client.dart'; -import 'package:contacts_plus_plus/models/friend.dart'; +import 'package:contacts_plus_plus/models/users/friend.dart'; import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:contacts_plus_plus/config.dart'; import 'package:contacts_plus_plus/models/message.dart'; diff --git a/lib/models/friend.dart b/lib/models/friend.dart deleted file mode 100644 index 97efed3..0000000 --- a/lib/models/friend.dart +++ /dev/null @@ -1,234 +0,0 @@ -import 'package:contacts_plus_plus/auxiliary.dart'; -import 'package:contacts_plus_plus/models/session.dart'; -import 'package:contacts_plus_plus/models/user_profile.dart'; -import 'package:flutter/material.dart'; - -class Friend implements Comparable { - static const _emptyId = "-1"; - static const _neosBotId = "U-Neos"; - final String id; - final String username; - final String ownerId; - final UserStatus userStatus; - final UserProfile userProfile; - final FriendStatus friendStatus; - final DateTime latestMessageTime; - - const Friend({required this.id, required this.username, required this.ownerId, required this.userStatus, required this.userProfile, - required this.friendStatus, required this.latestMessageTime, - }); - - bool get isHeadless => userStatus.activeSessions.any((session) => session.headlessHost == true && session.hostUserId == id); - - factory Friend.fromMap(Map map) { - final userStatus = UserStatus.fromMap(map["userStatus"]); - return Friend( - id: map["id"], - username: map["friendUsername"], - ownerId: map["ownerId"] ?? map["id"], - // Neos bot status is always offline but should be displayed as online - userStatus: map["id"] == _neosBotId ? userStatus.copyWith(onlineStatus: OnlineStatus.online) : userStatus, - userProfile: UserProfile.fromMap(map["profile"] ?? {}), - friendStatus: FriendStatus.fromString(map["friendStatus"]), - latestMessageTime: map["latestMessageTime"] == null - ? DateTime.fromMillisecondsSinceEpoch(0) : DateTime.parse(map["latestMessageTime"]), - ); - } - - static Friend? fromMapOrNull(Map? map) { - if (map == null) return null; - return Friend.fromMap(map); - } - - factory Friend.empty() { - return Friend( - id: _emptyId, - username: "", - ownerId: "", - userStatus: UserStatus.empty(), - userProfile: UserProfile.empty(), - friendStatus: FriendStatus.none, - latestMessageTime: DateTimeX.epoch - ); - } - - bool get isEmpty => id == _emptyId; - - Friend copyWith({ - String? id, String? username, String? ownerId, UserStatus? userStatus, UserProfile? userProfile, - FriendStatus? friendStatus, DateTime? latestMessageTime}) { - return Friend( - id: id ?? this.id, - username: username ?? this.username, - ownerId: ownerId ?? this.ownerId, - userStatus: userStatus ?? this.userStatus, - userProfile: userProfile ?? this.userProfile, - friendStatus: friendStatus ?? this.friendStatus, - latestMessageTime: latestMessageTime ?? this.latestMessageTime, - ); - } - - Map toMap({bool shallow=false}) { - return { - "id": id, - "username": username, - "ownerId": ownerId, - "userStatus": userStatus.toMap(shallow: shallow), - "profile": userProfile.toMap(), - "friendStatus": friendStatus.name, - "latestMessageTime": latestMessageTime.toIso8601String(), - }; - } - - @override - int compareTo(covariant Friend other) { - return username.compareTo(other.username); - } -} - -enum FriendStatus { - none, - searchResult, - requested, - ignored, - blocked, - accepted; - - factory FriendStatus.fromString(String text) { - return FriendStatus.values.firstWhere((element) => element.name.toLowerCase() == text.toLowerCase(), - orElse: () => FriendStatus.none, - ); - } -} - -enum OnlineStatus { - offline, - invisible, - away, - busy, - online; - - static final List _colors = [ - Colors.transparent, - Colors.transparent, - Colors.yellow, - Colors.red, - Colors.green, - ]; - - Color color(BuildContext context) => this == OnlineStatus.offline || this == OnlineStatus.invisible ? Theme.of(context).colorScheme.onSurface : _colors[index]; - - factory OnlineStatus.fromString(String? text) { - return OnlineStatus.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(), - orElse: () => OnlineStatus.offline, - ); - } - - int compareTo(OnlineStatus other) { - if (this == other) return 0; - if (this == OnlineStatus.online) return -1; - if (other == OnlineStatus.online) return 1; - if (this == OnlineStatus.away) return -1; - if (other == OnlineStatus.away) return 1; - if (this == OnlineStatus.busy) return -1; - if (other == OnlineStatus.busy) return 1; - return 0; - } -} - -class UserStatus { - final OnlineStatus onlineStatus; - final DateTime lastStatusChange; - final int currentSessionAccessLevel; - final bool currentSessionHidden; - final bool currentHosting; - final Session currentSession; - final List activeSessions; - final String neosVersion; - final String outputDevice; - final bool isMobile; - final String compatibilityHash; - - const UserStatus( - {required this.onlineStatus, required this.lastStatusChange, required this.currentSession, - required this.currentSessionAccessLevel, required this.currentSessionHidden, required this.currentHosting, - required this.activeSessions, required this.neosVersion, required this.outputDevice, required this.isMobile, - required this.compatibilityHash, - }); - - factory UserStatus.empty() => - UserStatus( - onlineStatus: OnlineStatus.offline, - lastStatusChange: DateTime.now(), - currentSessionAccessLevel: 0, - currentSessionHidden: false, - currentHosting: false, - currentSession: Session.none(), - activeSessions: [], - neosVersion: "", - outputDevice: "Unknown", - isMobile: false, - compatibilityHash: "", - ); - - factory UserStatus.fromMap(Map map) { - final statusString = map["onlineStatus"] as String?; - final status = OnlineStatus.fromString(statusString); - return UserStatus( - onlineStatus: status, - lastStatusChange: DateTime.parse(map["lastStatusChange"]), - currentSessionAccessLevel: map["currentSessionAccessLevel"] ?? 0, - currentSessionHidden: map["currentSessionHidden"] ?? false, - currentHosting: map["currentHosting"] ?? false, - currentSession: Session.fromMap(map["currentSession"]), - activeSessions: (map["activeSessions"] as List? ?? []).map((e) => Session.fromMap(e)).toList(), - neosVersion: map["neosVersion"] ?? "", - outputDevice: map["outputDevice"] ?? "Unknown", - isMobile: map["isMobile"] ?? false, - compatibilityHash: map["compatabilityHash"] ?? "" - ); - } - - Map toMap({bool shallow = false}) { - return { - "onlineStatus": onlineStatus.index, - "lastStatusChange": lastStatusChange.toIso8601String(), - "currentSessionAccessLevel": currentSessionAccessLevel, - "currentSessionHidden": currentSessionHidden, - "currentHosting": currentHosting, - "currentSession": currentSession.isNone || shallow ? null : currentSession.toMap(), - "activeSessions": shallow ? [] : activeSessions.map((e) => e.toMap(),).toList(), - "neosVersion": neosVersion, - "outputDevice": outputDevice, - "isMobile": isMobile, - "compatibilityHash": compatibilityHash, - }; - } - - UserStatus copyWith({ - OnlineStatus? onlineStatus, - DateTime? lastStatusChange, - int? currentSessionAccessLevel, - bool? currentSessionHidden, - bool? currentHosting, - Session? currentSession, - List? activeSessions, - String? neosVersion, - String? outputDevice, - bool? isMobile, - String? compatibilityHash, - }) => - UserStatus( - onlineStatus: onlineStatus ?? this.onlineStatus, - lastStatusChange: lastStatusChange ?? this.lastStatusChange, - currentSessionAccessLevel: currentSessionAccessLevel ?? this.currentSessionAccessLevel, - currentSessionHidden: currentSessionHidden ?? this.currentSessionHidden, - currentHosting: currentHosting ?? this.currentHosting, - currentSession: currentSession ?? this.currentSession, - activeSessions: activeSessions ?? this.activeSessions, - neosVersion: neosVersion ?? this.neosVersion, - outputDevice: outputDevice ?? this.outputDevice, - isMobile: isMobile ?? this.isMobile, - compatibilityHash: compatibilityHash ?? this.compatibilityHash, - ); -} \ No newline at end of file diff --git a/lib/models/personal_profile.dart b/lib/models/personal_profile.dart index b2833fd..8de1607 100644 --- a/lib/models/personal_profile.dart +++ b/lib/models/personal_profile.dart @@ -1,4 +1,4 @@ -import 'package:contacts_plus_plus/models/user_profile.dart'; +import 'package:contacts_plus_plus/models/users/user_profile.dart'; class PersonalProfile { final String id; diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 97484d7..a5410a2 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -1,7 +1,7 @@ import 'dart:convert'; -import 'package:contacts_plus_plus/models/friend.dart'; import 'package:contacts_plus_plus/models/sem_ver.dart'; +import 'package:contacts_plus_plus/models/users/online_status.dart'; import 'package:flutter/material.dart'; import 'package:uuid/uuid.dart'; diff --git a/lib/models/users/friend.dart b/lib/models/users/friend.dart new file mode 100644 index 0000000..1e5684f --- /dev/null +++ b/lib/models/users/friend.dart @@ -0,0 +1,88 @@ +import 'package:contacts_plus_plus/auxiliary.dart'; +import 'package:contacts_plus_plus/models/users/user_profile.dart'; +import 'package:contacts_plus_plus/models/users/friend_status.dart'; +import 'package:contacts_plus_plus/models/users/online_status.dart'; +import 'package:contacts_plus_plus/models/users/user_status.dart'; + +class Friend implements Comparable { + static const _emptyId = "-1"; + static const _neosBotId = "U-Neos"; + final String id; + final String username; + final String ownerId; + final UserStatus userStatus; + final UserProfile userProfile; + final FriendStatus friendStatus; + final DateTime latestMessageTime; + + const Friend({required this.id, required this.username, required this.ownerId, required this.userStatus, required this.userProfile, + required this.friendStatus, required this.latestMessageTime, + }); + + bool get isHeadless => userStatus.activeSessions.any((session) => session.headlessHost == true && session.hostUserId == id); + + factory Friend.fromMap(Map map) { + final userStatus = UserStatus.fromMap(map["userStatus"]); + return Friend( + id: map["id"], + username: map["friendUsername"], + ownerId: map["ownerId"] ?? map["id"], + // Neos bot status is always offline but should be displayed as online + userStatus: map["id"] == _neosBotId ? userStatus.copyWith(onlineStatus: OnlineStatus.online) : userStatus, + userProfile: UserProfile.fromMap(map["profile"] ?? {}), + friendStatus: FriendStatus.fromString(map["friendStatus"]), + latestMessageTime: map["latestMessageTime"] == null + ? DateTime.fromMillisecondsSinceEpoch(0) : DateTime.parse(map["latestMessageTime"]), + ); + } + + static Friend? fromMapOrNull(Map? map) { + if (map == null) return null; + return Friend.fromMap(map); + } + + factory Friend.empty() { + return Friend( + id: _emptyId, + username: "", + ownerId: "", + userStatus: UserStatus.empty(), + userProfile: UserProfile.empty(), + friendStatus: FriendStatus.none, + latestMessageTime: DateTimeX.epoch + ); + } + + bool get isEmpty => id == _emptyId; + + Friend copyWith({ + String? id, String? username, String? ownerId, UserStatus? userStatus, UserProfile? userProfile, + FriendStatus? friendStatus, DateTime? latestMessageTime}) { + return Friend( + id: id ?? this.id, + username: username ?? this.username, + ownerId: ownerId ?? this.ownerId, + userStatus: userStatus ?? this.userStatus, + userProfile: userProfile ?? this.userProfile, + friendStatus: friendStatus ?? this.friendStatus, + latestMessageTime: latestMessageTime ?? this.latestMessageTime, + ); + } + + Map toMap({bool shallow=false}) { + return { + "id": id, + "username": username, + "ownerId": ownerId, + "userStatus": userStatus.toMap(shallow: shallow), + "profile": userProfile.toMap(), + "friendStatus": friendStatus.name, + "latestMessageTime": latestMessageTime.toIso8601String(), + }; + } + + @override + int compareTo(covariant Friend other) { + return username.compareTo(other.username); + } +} diff --git a/lib/models/users/friend_status.dart b/lib/models/users/friend_status.dart new file mode 100644 index 0000000..1177b2d --- /dev/null +++ b/lib/models/users/friend_status.dart @@ -0,0 +1,14 @@ +enum FriendStatus { + none, + searchResult, + requested, + ignored, + blocked, + accepted; + + factory FriendStatus.fromString(String text) { + return FriendStatus.values.firstWhere((element) => element.name.toLowerCase() == text.toLowerCase(), + orElse: () => FriendStatus.none, + ); + } +} diff --git a/lib/models/users/online_status.dart b/lib/models/users/online_status.dart new file mode 100644 index 0000000..97662b6 --- /dev/null +++ b/lib/models/users/online_status.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +enum OnlineStatus { + offline, + invisible, + away, + busy, + online; + + static final List _colors = [ + Colors.transparent, + Colors.transparent, + Colors.yellow, + Colors.red, + Colors.green, + ]; + + Color color(BuildContext context) => this == OnlineStatus.offline || this == OnlineStatus.invisible ? Theme.of(context).colorScheme.onSurface : _colors[index]; + + factory OnlineStatus.fromString(String? text) { + return OnlineStatus.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(), + orElse: () => OnlineStatus.offline, + ); + } + + int compareTo(OnlineStatus other) { + if (this == other) return 0; + if (this == OnlineStatus.online) return -1; + if (other == OnlineStatus.online) return 1; + if (this == OnlineStatus.away) return -1; + if (other == OnlineStatus.away) return 1; + if (this == OnlineStatus.busy) return -1; + if (other == OnlineStatus.busy) return 1; + return 0; + } +} diff --git a/lib/models/user.dart b/lib/models/users/user.dart similarity index 91% rename from lib/models/user.dart rename to lib/models/users/user.dart index 01385c2..9188643 100644 --- a/lib/models/user.dart +++ b/lib/models/users/user.dart @@ -1,4 +1,4 @@ -import 'package:contacts_plus_plus/models/user_profile.dart'; +import 'package:contacts_plus_plus/models/users/user_profile.dart'; class User { final String id; diff --git a/lib/models/user_profile.dart b/lib/models/users/user_profile.dart similarity index 100% rename from lib/models/user_profile.dart rename to lib/models/users/user_profile.dart diff --git a/lib/models/users/user_status.dart b/lib/models/users/user_status.dart new file mode 100644 index 0000000..d3a2176 --- /dev/null +++ b/lib/models/users/user_status.dart @@ -0,0 +1,99 @@ +import 'package:contacts_plus_plus/models/session.dart'; +import 'package:contacts_plus_plus/models/users/online_status.dart'; + +class UserStatus { + final OnlineStatus onlineStatus; + final DateTime lastStatusChange; + final int currentSessionAccessLevel; + final bool currentSessionHidden; + final bool currentHosting; + final Session currentSession; + final List activeSessions; + final String neosVersion; + final String outputDevice; + final bool isMobile; + final String compatibilityHash; + + const UserStatus( + {required this.onlineStatus, required this.lastStatusChange, required this.currentSession, + required this.currentSessionAccessLevel, required this.currentSessionHidden, required this.currentHosting, + required this.activeSessions, required this.neosVersion, required this.outputDevice, required this.isMobile, + required this.compatibilityHash, + }); + + factory UserStatus.empty() => + UserStatus( + onlineStatus: OnlineStatus.offline, + lastStatusChange: DateTime.now(), + currentSessionAccessLevel: 0, + currentSessionHidden: false, + currentHosting: false, + currentSession: Session.none(), + activeSessions: [], + neosVersion: "", + outputDevice: "Unknown", + isMobile: false, + compatibilityHash: "", + ); + + factory UserStatus.fromMap(Map map) { + final statusString = map["onlineStatus"] as String?; + final status = OnlineStatus.fromString(statusString); + return UserStatus( + onlineStatus: status, + lastStatusChange: DateTime.parse(map["lastStatusChange"]), + currentSessionAccessLevel: map["currentSessionAccessLevel"] ?? 0, + currentSessionHidden: map["currentSessionHidden"] ?? false, + currentHosting: map["currentHosting"] ?? false, + currentSession: Session.fromMap(map["currentSession"]), + activeSessions: (map["activeSessions"] as List? ?? []).map((e) => Session.fromMap(e)).toList(), + neosVersion: map["neosVersion"] ?? "", + outputDevice: map["outputDevice"] ?? "Unknown", + isMobile: map["isMobile"] ?? false, + compatibilityHash: map["compatabilityHash"] ?? "" + ); + } + + Map toMap({bool shallow = false}) { + return { + "onlineStatus": onlineStatus.index, + "lastStatusChange": lastStatusChange.toIso8601String(), + "currentSessionAccessLevel": currentSessionAccessLevel, + "currentSessionHidden": currentSessionHidden, + "currentHosting": currentHosting, + "currentSession": currentSession.isNone || shallow ? null : currentSession.toMap(), + "activeSessions": shallow ? [] : activeSessions.map((e) => e.toMap(),).toList(), + "neosVersion": neosVersion, + "outputDevice": outputDevice, + "isMobile": isMobile, + "compatibilityHash": compatibilityHash, + }; + } + + UserStatus copyWith({ + OnlineStatus? onlineStatus, + DateTime? lastStatusChange, + int? currentSessionAccessLevel, + bool? currentSessionHidden, + bool? currentHosting, + Session? currentSession, + List? activeSessions, + String? neosVersion, + String? outputDevice, + bool? isMobile, + String? compatibilityHash, + }) => + UserStatus( + onlineStatus: onlineStatus ?? this.onlineStatus, + lastStatusChange: lastStatusChange ?? this.lastStatusChange, + currentSessionAccessLevel: currentSessionAccessLevel ?? this.currentSessionAccessLevel, + currentSessionHidden: currentSessionHidden ?? this.currentSessionHidden, + currentHosting: currentHosting ?? this.currentHosting, + currentSession: currentSession ?? this.currentSession, + activeSessions: activeSessions ?? this.activeSessions, + neosVersion: neosVersion ?? this.neosVersion, + outputDevice: outputDevice ?? this.outputDevice, + isMobile: isMobile ?? this.isMobile, + compatibilityHash: compatibilityHash ?? this.compatibilityHash, + ); +} \ No newline at end of file diff --git a/lib/widgets/friends/friend_list_tile.dart b/lib/widgets/friends/friend_list_tile.dart index 140ce31..0de3a65 100644 --- a/lib/widgets/friends/friend_list_tile.dart +++ b/lib/widgets/friends/friend_list_tile.dart @@ -1,6 +1,6 @@ import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:contacts_plus_plus/clients/messaging_client.dart'; -import 'package:contacts_plus_plus/models/friend.dart'; +import 'package:contacts_plus_plus/models/users/friend.dart'; import 'package:contacts_plus_plus/models/message.dart'; import 'package:contacts_plus_plus/widgets/formatted_text.dart'; import 'package:contacts_plus_plus/widgets/friends/friend_online_status_indicator.dart'; @@ -83,7 +83,7 @@ class FriendListTile extends StatelessWidget { MaterialPageRoute( builder: (context) => ChangeNotifierProvider.value( value: mClient, - child: MessagesList(), + child: const MessagesList(), ), ), ); diff --git a/lib/widgets/friends/friend_online_status_indicator.dart b/lib/widgets/friends/friend_online_status_indicator.dart index 9b374fb..12d2f3a 100644 --- a/lib/widgets/friends/friend_online_status_indicator.dart +++ b/lib/widgets/friends/friend_online_status_indicator.dart @@ -1,4 +1,5 @@ -import 'package:contacts_plus_plus/models/friend.dart'; +import 'package:contacts_plus_plus/models/users/online_status.dart'; +import 'package:contacts_plus_plus/models/users/user_status.dart'; import 'package:flutter/material.dart'; class FriendOnlineStatusIndicator extends StatelessWidget { diff --git a/lib/widgets/friends/friends_list.dart b/lib/widgets/friends/friends_list.dart index 2c70ba0..8549fcb 100644 --- a/lib/widgets/friends/friends_list.dart +++ b/lib/widgets/friends/friends_list.dart @@ -3,7 +3,8 @@ 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/clients/messaging_client.dart'; -import 'package:contacts_plus_plus/models/friend.dart'; +import 'package:contacts_plus_plus/models/users/online_status.dart'; +import 'package:contacts_plus_plus/models/users/user_status.dart'; import 'package:contacts_plus_plus/widgets/default_error_widget.dart'; import 'package:contacts_plus_plus/widgets/friends/expanding_input_fab.dart'; import 'package:contacts_plus_plus/widgets/friends/friend_list_tile.dart'; diff --git a/lib/widgets/friends/user_list_tile.dart b/lib/widgets/friends/user_list_tile.dart index f7614e3..d9d621c 100644 --- a/lib/widgets/friends/user_list_tile.dart +++ b/lib/widgets/friends/user_list_tile.dart @@ -1,7 +1,7 @@ import 'package:contacts_plus_plus/apis/user_api.dart'; import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:contacts_plus_plus/client_holder.dart'; -import 'package:contacts_plus_plus/models/user.dart'; +import 'package:contacts_plus_plus/models/users/user.dart'; import 'package:contacts_plus_plus/widgets/generic_avatar.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; diff --git a/lib/widgets/friends/user_search.dart b/lib/widgets/friends/user_search.dart index 89d1442..94ab726 100644 --- a/lib/widgets/friends/user_search.dart +++ b/lib/widgets/friends/user_search.dart @@ -3,7 +3,7 @@ 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/clients/messaging_client.dart'; -import 'package:contacts_plus_plus/models/user.dart'; +import 'package:contacts_plus_plus/models/users/user.dart'; import 'package:contacts_plus_plus/widgets/default_error_widget.dart'; import 'package:contacts_plus_plus/widgets/friends/user_list_tile.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/messages/message_input_bar.dart b/lib/widgets/messages/message_input_bar.dart index 6fffaa5..1eae33c 100644 --- a/lib/widgets/messages/message_input_bar.dart +++ b/lib/widgets/messages/message_input_bar.dart @@ -7,7 +7,7 @@ import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:contacts_plus_plus/client_holder.dart'; import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:contacts_plus_plus/clients/messaging_client.dart'; -import 'package:contacts_plus_plus/models/friend.dart'; +import 'package:contacts_plus_plus/models/users/friend.dart'; import 'package:contacts_plus_plus/models/message.dart'; import 'package:contacts_plus_plus/widgets/messages/message_attachment_list.dart'; import 'package:file_picker/file_picker.dart'; diff --git a/lib/widgets/messages/message_session_invite.dart b/lib/widgets/messages/message_session_invite.dart index f0fc9b3..ecc0453 100644 --- a/lib/widgets/messages/message_session_invite.dart +++ b/lib/widgets/messages/message_session_invite.dart @@ -7,6 +7,7 @@ import 'package:contacts_plus_plus/widgets/formatted_text.dart'; import 'package:contacts_plus_plus/widgets/generic_avatar.dart'; import 'package:contacts_plus_plus/widgets/messages/messages_session_header.dart'; import 'package:contacts_plus_plus/widgets/messages/message_state_indicator.dart'; +import 'package:contacts_plus_plus/widgets/session_view.dart'; import 'package:flutter/material.dart'; class MessageSessionInvite extends StatelessWidget { @@ -22,7 +23,7 @@ class MessageSessionInvite extends StatelessWidget { constraints: const BoxConstraints(maxWidth: 300), child: TextButton( onPressed: () { - showDialog(context: context, builder: (context) => SessionPopup(session: sessionInfo)); + Navigator.of(context).push(MaterialPageRoute(builder: (context) => SessionView(session: sessionInfo))); }, style: TextButton.styleFrom(padding: EdgeInsets.zero), child: Container( @@ -44,7 +45,7 @@ class MessageSessionInvite extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ GenericAvatar( - imageUri: Aux.neosDbToHttp(Aux.neosDbToHttp(sessionInfo.thumbnail)), + imageUri: Aux.neosDbToHttp(sessionInfo.thumbnail), placeholderIcon: Icons.no_photography, foregroundColor: foregroundColor, ), diff --git a/lib/widgets/messages/messages_list.dart b/lib/widgets/messages/messages_list.dart index a98a5e6..450e346 100644 --- a/lib/widgets/messages/messages_list.dart +++ b/lib/widgets/messages/messages_list.dart @@ -1,6 +1,6 @@ import 'package:contacts_plus_plus/clients/audio_cache_client.dart'; import 'package:contacts_plus_plus/clients/messaging_client.dart'; -import 'package:contacts_plus_plus/models/friend.dart'; +import 'package:contacts_plus_plus/models/users/friend.dart'; import 'package:contacts_plus_plus/widgets/default_error_widget.dart'; import 'package:contacts_plus_plus/widgets/friends/friend_online_status_indicator.dart'; import 'package:contacts_plus_plus/widgets/messages/message_input_bar.dart'; @@ -21,7 +21,7 @@ class _MessagesListState extends State with SingleTickerProviderSt final ScrollController _sessionListScrollController = ScrollController(); bool _showSessionListScrollChevron = false; - bool _sessionListOpen = false; + bool _sessionListOpen = true; double get _shevronOpacity => _showSessionListScrollChevron ? 1.0 : 0.0; diff --git a/lib/widgets/messages/messages_session_header.dart b/lib/widgets/messages/messages_session_header.dart index 862687a..1c3a4e0 100644 --- a/lib/widgets/messages/messages_session_header.dart +++ b/lib/widgets/messages/messages_session_header.dart @@ -3,6 +3,7 @@ import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:contacts_plus_plus/models/session.dart'; import 'package:contacts_plus_plus/widgets/formatted_text.dart'; import 'package:contacts_plus_plus/widgets/generic_avatar.dart'; +import 'package:contacts_plus_plus/widgets/session_view.dart'; import 'package:flutter/material.dart'; class SessionPopup extends StatelessWidget { @@ -39,7 +40,7 @@ class SessionPopup extends StatelessWidget { style: Theme.of(context).textTheme.labelMedium, softWrap: true, ), - Text("Access: ${session.accessLevel.toReadableString()}"), + Text("Access: ${session.accessLevel.toReadableString()}", style: Theme.of(context).textTheme.labelMedium), Text("Users: ${session.sessionUsers.length}", style: Theme.of(context).textTheme.labelMedium), Text("Maximum users: ${session.maxUsers}", style: Theme.of(context).textTheme.labelMedium), Text("Headless: ${session.headlessHost ? "Yes" : "No"}", style: Theme.of(context).textTheme.labelMedium), @@ -119,7 +120,7 @@ class SessionTile extends StatelessWidget { Widget build(BuildContext context) { return TextButton( onPressed: () { - showDialog(context: context, builder: (context) => SessionPopup(session: session)); + Navigator.of(context).push(MaterialPageRoute(builder: (context) => SessionView(session: session))); }, child: Row( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/widgets/session_view.dart b/lib/widgets/session_view.dart new file mode 100644 index 0000000..a31447a --- /dev/null +++ b/lib/widgets/session_view.dart @@ -0,0 +1,185 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:contacts_plus_plus/auxiliary.dart'; +import 'package:contacts_plus_plus/models/session.dart'; +import 'package:contacts_plus_plus/widgets/formatted_text.dart'; +import 'package:contacts_plus_plus/widgets/settings_page.dart'; +import 'package:flutter/material.dart'; +import 'package:photo_view/photo_view.dart'; + +class SessionView extends StatelessWidget { + const SessionView({required this.session, super.key}); + + final Session session; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: CustomScrollView( + physics: const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast), + slivers: [ + SliverAppBar( + leading: IconButton( + icon: const Icon( + Icons.arrow_back_outlined, + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + pinned: true, + snap: false, + floating: false, + expandedHeight: 192, + surfaceTintColor: Theme.of(context).colorScheme.surfaceVariant, + centerTitle: true, + title: FormattedText( + session.formattedName, + maxLines: 1, + style: Theme.of(context).textTheme.titleLarge, + ), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1), + child: Row( + children: [ + Expanded( + child: Container( + width: double.infinity, + height: 1, + color: Colors.black, + )), + ], + ), + ), + flexibleSpace: FlexibleSpaceBar( + collapseMode: CollapseMode.pin, + background: CachedNetworkImage( + imageUrl: Aux.neosDbToHttp(session.thumbnail), + imageBuilder: (context, image) { + return InkWell( + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PhotoView( + minScale: PhotoViewComputedScale.contained, + imageProvider: image, + heroAttributes: PhotoViewHeroAttributes(tag: session.id), + ), + ), + ); + }, + child: Hero( + tag: session.id, + child: Image( + image: image, + fit: BoxFit.cover, + ), + ), + ); + }, + errorWidget: (context, url, error) => const Icon( + Icons.broken_image, + size: 64, + ), + placeholder: (context, uri) => const Center(child: CircularProgressIndicator()), + ), + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(top: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 8), + child: session.formattedDescription.isEmpty + ? Text("No description", style: Theme.of(context).textTheme.labelLarge) + : FormattedText( + session.formattedDescription, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + const ListSectionHeader( + leadingText: "Tags:", + showLine: false, + ), + Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 8.0), + child: Text( + session.tags.isEmpty ? "None" : session.tags.join(", "), + style: Theme.of(context).textTheme.labelMedium, + textAlign: TextAlign.start, + softWrap: true, + ), + ), + const ListSectionHeader( + leadingText: "Details:", + showLine: false, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Access: ", + style: Theme.of(context).textTheme.labelLarge, + ), + Text( + session.accessLevel.toReadableString(), + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Headless: ", + style: Theme.of(context).textTheme.labelLarge, + ), + Text( + session.headlessHost ? "Yes" : "No", + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + ), + ListSectionHeader( + leadingText: "Users", + trailingText: + "${session.sessionUsers.length.toString().padLeft(2, "0")}/${session.maxUsers.toString().padLeft(2, "0")}", + showLine: false, + ), + ], + ), + ), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + final user = session.sessionUsers[index % session.sessionUsers.length]; + return ListTile( + dense: true, + title: Text( + user.username, + textAlign: TextAlign.start, + ), + subtitle: Text( + user.isPresent ? "Active" : "Inactive", + textAlign: TextAlign.start, + ), + ); + }, + childCount: session.sessionUsers.length * 4, + ), + ) + ], + ), + ); + } +} diff --git a/lib/widgets/settings_page.dart b/lib/widgets/settings_page.dart index 8a07882..f72ab9f 100644 --- a/lib/widgets/settings_page.dart +++ b/lib/widgets/settings_page.dart @@ -23,60 +23,62 @@ class SettingsPage extends StatelessWidget { ), body: ListView( children: [ - const ListSectionHeader(name: "Notifications"), + const ListSectionHeader(leadingText: "Notifications"), BooleanSettingsTile( title: "Enable Notifications", initialState: !sClient.currentSettings.notificationsDenied.valueOrDefault, - onChanged: (value) async => await sClient.changeSettings(sClient.currentSettings.copyWith(notificationsDenied: !value)), + onChanged: (value) async => + await sClient.changeSettings(sClient.currentSettings.copyWith(notificationsDenied: !value)), ), - const ListSectionHeader(name: "Appearance"), + const ListSectionHeader(leadingText: "Appearance"), ListTile( - trailing: StatefulBuilder( - builder: (context, setState) { - return DropdownButton( - items: ThemeMode.values.map((mode) => DropdownMenuItem( - value: mode, - child: Text("${toBeginningOfSentenceCase(mode.name)}",), - )).toList(), - value: ThemeMode.values[sClient.currentSettings.themeMode.valueOrDefault], - onChanged: (ThemeMode? value) async { - final currentSetting = sClient.currentSettings.themeMode.value; - if (currentSetting != value?.index) { - await sClient.changeSettings(sClient.currentSettings.copyWith(themeMode: value?.index)); - if (context.mounted) { - Phoenix.rebirth(context); - } - } - setState(() {}); - }, - ); - } - ), + trailing: StatefulBuilder(builder: (context, setState) { + return DropdownButton( + items: ThemeMode.values + .map((mode) => DropdownMenuItem( + value: mode, + child: Text( + "${toBeginningOfSentenceCase(mode.name)}", + ), + )) + .toList(), + value: ThemeMode.values[sClient.currentSettings.themeMode.valueOrDefault], + onChanged: (ThemeMode? value) async { + final currentSetting = sClient.currentSettings.themeMode.value; + if (currentSetting != value?.index) { + await sClient.changeSettings(sClient.currentSettings.copyWith(themeMode: value?.index)); + if (context.mounted) { + Phoenix.rebirth(context); + } + } + setState(() {}); + }, + ); + }), title: const Text("Theme Mode"), ), - const ListSectionHeader(name: "Other"), + const ListSectionHeader(leadingText: "Other"), ListTile( trailing: const Icon(Icons.logout), title: const Text("Sign out"), onTap: () { showDialog( context: context, - builder: (context) => - AlertDialog( - title: Text("Are you sure you want to sign out?", style: Theme - .of(context) - .textTheme - .titleLarge,), - actions: [ - TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text("No")), - TextButton( - onPressed: () async { - await ClientHolder.of(context).apiClient.logout(); - }, - child: const Text("Yes"), - ), - ], + builder: (context) => AlertDialog( + title: Text( + "Are you sure you want to sign out?", + style: Theme.of(context).textTheme.titleLarge, + ), + actions: [ + TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text("No")), + TextButton( + onPressed: () async { + await ClientHolder.of(context).apiClient.logout(); + }, + child: const Text("Yes"), ), + ], + ), ); }, ), @@ -89,9 +91,11 @@ class SettingsPage extends StatelessWidget { applicationVersion: (await PackageInfo.fromPlatform()).version, applicationIcon: InkWell( onTap: () async { - if (!await launchUrl(Uri.parse("https://github.com/Nutcake/contacts-plus-plus"), mode: LaunchMode.externalApplication)) { + if (!await launchUrl(Uri.parse("https://github.com/Nutcake/contacts-plus-plus"), + mode: LaunchMode.externalApplication)) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Failed to open link."))); + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text("Failed to open link."))); } } }, @@ -112,26 +116,35 @@ class SettingsPage extends StatelessWidget { } class ListSectionHeader extends StatelessWidget { - const ListSectionHeader({required this.name, super.key}); + const ListSectionHeader({required this.leadingText, this.trailingText, this.showLine = true, super.key}); - final String name; + final String leadingText; + final String? trailingText; + final bool showLine; @override Widget build(BuildContext context) { - final theme = Theme.of(context); + final textTheme = Theme.of(context).textTheme.labelSmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ); return Padding( padding: const EdgeInsets.all(8.0), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text(name, style: theme.textTheme.labelSmall?.copyWith(color: theme.colorScheme.onSurfaceVariant)), + Text(leadingText, style: textTheme), Expanded( child: Container( margin: const EdgeInsets.symmetric(horizontal: 8), color: Colors.white12, - height: 1, + height: showLine ? 1 : 0, + ), + ), + if (trailingText != null) + Text( + trailingText!, + style: textTheme, ), - ) ], ), ); @@ -173,5 +186,4 @@ class _BooleanSettingsTileState extends State { }, ); } - -} \ No newline at end of file +} diff --git a/pubspec.lock b/pubspec.lock index 4d7300d..eb9527d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -998,5 +998,5 @@ packages: source: hosted version: "6.3.0" sdks: - dart: ">=3.0.0 <4.0.0" + dart: ">=3.0.1 <4.0.0" flutter: ">=3.4.0-17.0.pre" diff --git a/pubspec.yaml b/pubspec.yaml index ba9a30b..d9c769a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,10 +16,10 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.3.1+1 +version: 1.3.2+1 environment: - sdk: '>=3.0.0' + sdk: '>=3.0.1' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions