From 24178aaa437cf964d753ee2babe18f447ff7359f Mon Sep 17 00:00:00 2001 From: Nutcake Date: Sat, 3 Jun 2023 17:17:54 +0200 Subject: [PATCH] Add tabbed view to homescreen --- lib/clients/messaging_client.dart | 2 + lib/main.dart | 179 ++++++--- lib/widgets/friends/friends_list.dart | 340 ++++-------------- lib/widgets/friends/friends_list_app_bar.dart | 223 ++++++++++++ lib/widgets/global_app_bar.dart | 20 ++ .../messages/message_session_invite.dart | 1 - lib/widgets/session_list.dart | 226 ++++++------ lib/widgets/session_list_app_bar.dart | 20 ++ lib/widgets/settings_app_bar.dart | 13 + lib/widgets/settings_page.dart | 15 +- 10 files changed, 576 insertions(+), 463 deletions(-) create mode 100644 lib/widgets/friends/friends_list_app_bar.dart create mode 100644 lib/widgets/global_app_bar.dart create mode 100644 lib/widgets/session_list_app_bar.dart create mode 100644 lib/widgets/settings_app_bar.dart diff --git a/lib/clients/messaging_client.dart b/lib/clients/messaging_client.dart index 0203e76..2297d25 100644 --- a/lib/clients/messaging_client.dart +++ b/lib/clients/messaging_client.dart @@ -68,6 +68,7 @@ class MessagingClient extends ChangeNotifier { MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient}) : _apiClient = apiClient, _notificationClient = notificationClient { + debugPrint("mClient created: $hashCode"); Hive.openBox(_messageBoxKey).then((box) async { box.delete(_lastUpdateKey); await refreshFriendsListWithErrorHandler(); @@ -84,6 +85,7 @@ class MessagingClient extends ChangeNotifier { @override void dispose() { + debugPrint("mClient disposed: $hashCode"); _autoRefresh?.cancel(); _notifyOnlineTimer?.cancel(); _unreadSafeguard?.cancel(); diff --git a/lib/main.dart b/lib/main.dart index ef86464..8f3a027 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,7 +7,12 @@ import 'package:contacts_plus_plus/clients/messaging_client.dart'; import 'package:contacts_plus_plus/clients/settings_client.dart'; import 'package:contacts_plus_plus/models/sem_ver.dart'; import 'package:contacts_plus_plus/widgets/friends/friends_list.dart'; +import 'package:contacts_plus_plus/widgets/friends/friends_list_app_bar.dart'; import 'package:contacts_plus_plus/widgets/login_screen.dart'; +import 'package:contacts_plus_plus/widgets/session_list.dart'; +import 'package:contacts_plus_plus/widgets/session_list_app_bar.dart'; +import 'package:contacts_plus_plus/widgets/settings_app_bar.dart'; +import 'package:contacts_plus_plus/widgets/settings_page.dart'; import 'package:contacts_plus_plus/widgets/update_notifier.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; @@ -22,12 +27,15 @@ import 'models/authentication_data.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + Provider.debugCheckInvalidValueType = null; await Hive.initFlutter(); final dateFormat = DateFormat.Hms(); - Logger.root.onRecord.listen((event) => log("${dateFormat.format(event.time)}: ${event.message}", name: event.loggerName, time: event.time)); + Logger.root.onRecord.listen( + (event) => log("${dateFormat.format(event.time)}: ${event.message}", name: event.loggerName, time: event.time)); final settingsClient = SettingsClient(); await settingsClient.loadSettings(); - final newSettings = settingsClient.currentSettings.copyWith(machineId: settingsClient.currentSettings.machineId.valueOrDefault); + final newSettings = + settingsClient.currentSettings.copyWith(machineId: settingsClient.currentSettings.machineId.valueOrDefault); await settingsClient.changeSettings(newSettings); // Save generated machineId to disk AuthenticationData cachedAuth = AuthenticationData.unauthenticated(); try { @@ -47,15 +55,28 @@ class ContactsPlusPlus extends StatefulWidget { } class _ContactsPlusPlusState extends State { + static const List _appBars = [ + FriendsListAppBar( + key: ValueKey("friends_list_app_bar"), + ), + SessionListAppBar( + key: ValueKey("session_list_app_bar"), + ), + SettingsAppBar( + key: ValueKey("settings_app_bar"), + ) + ]; + final Typography _typography = Typography.material2021(platform: TargetPlatform.android); + final PageController _pageController = PageController(); late AuthenticationData _authData = widget.cachedAuthentication; + bool _checkedForUpdate = false; + int _selectedPage = 0; void showUpdateDialogOnFirstBuild(BuildContext context) { final navigator = Navigator.of(context); - final settings = ClientHolder - .of(context) - .settingsClient; + final settings = ClientHolder.of(context).settingsClient; if (_checkedForUpdate) return; _checkedForUpdate = true; GithubApi.getLatestTagName().then((remoteVer) async { @@ -103,61 +124,103 @@ class _ContactsPlusPlusState extends State { @override Widget build(BuildContext context) { return Phoenix( - child: Builder( - builder: (context) { - return ClientHolder( - settingsClient: widget.settingsClient, - authenticationData: _authData, - onLogout: () { - setState(() { - _authData = AuthenticationData.unauthenticated(); - }); - Phoenix.rebirth(context); - }, - child: DynamicColorBuilder( - builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) => MaterialApp( - debugShowCheckedModeBanner: false, - title: 'Contacts++', - theme: ThemeData( - useMaterial3: true, - textTheme: _typography.black, - colorScheme: lightDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.light), - ), - darkTheme: ThemeData( - useMaterial3: true, - textTheme: _typography.white, - colorScheme: darkDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark), - ), - themeMode: ThemeMode.values[widget.settingsClient.currentSettings.themeMode.valueOrDefault], - home: Builder( // Builder is necessary here since we need a context which has access to the ClientHolder + child: Builder(builder: (context) { + return ClientHolder( + settingsClient: widget.settingsClient, + authenticationData: _authData, + onLogout: () { + setState(() { + _authData = AuthenticationData.unauthenticated(); + }); + Phoenix.rebirth(context); + }, + child: DynamicColorBuilder( + builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) => MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Contacts++', + theme: ThemeData( + useMaterial3: true, + textTheme: _typography.black, + colorScheme: + lightDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.light), + ), + darkTheme: ThemeData( + useMaterial3: true, + textTheme: _typography.white, + colorScheme: + darkDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark), + ), + themeMode: ThemeMode.values[widget.settingsClient.currentSettings.themeMode.valueOrDefault], + home: Builder(// Builder is necessary here since we need a context which has access to the ClientHolder builder: (context) { - showUpdateDialogOnFirstBuild(context); - final clientHolder = ClientHolder.of(context); - return _authData.isAuthenticated ? - ChangeNotifierProvider( // This doesn't need to be a proxy provider since the arguments should never change during it's lifetime. - create: (context) => - MessagingClient( - apiClient: clientHolder.apiClient, - notificationClient: clientHolder.notificationClient, + showUpdateDialogOnFirstBuild(context); + final clientHolder = ClientHolder.of(context); + return _authData.isAuthenticated + ? Provider( + create: (context) => MessagingClient( + apiClient: clientHolder.apiClient, + notificationClient: clientHolder.notificationClient, + ), + dispose: (context, value) => value.dispose(), + child: Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: _appBars[_selectedPage], + ), ), - child: const FriendsList(), - ) : - LoginScreen( - onLoginSuccessful: (AuthenticationData authData) async { - if (authData.isAuthenticated) { - setState(() { - _authData = authData; - }); - } - }, - ); - } - ) - ), - ), - ); - } - ), + body: PageView( + controller: _pageController, + children: const [ + FriendsList(), + SessionList(), + SettingsPage(), + ], + ), + bottomNavigationBar: BottomNavigationBar( + selectedItemColor: Theme.of(context).colorScheme.primary, + currentIndex: _selectedPage, + onTap: (index) { + _pageController.animateToPage( + index, + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + ); + setState(() { + _selectedPage = index; + }); + }, + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.message), + label: "Chat", + ), + BottomNavigationBarItem( + icon: Icon(Icons.public), + label: "Sessions", + ), + BottomNavigationBarItem( + icon: Icon(Icons.settings), + label: "Settings", + ), + ], + ), + ), + ) + : LoginScreen( + onLoginSuccessful: (AuthenticationData authData) async { + if (authData.isAuthenticated) { + setState(() { + _authData = authData; + }); + } + }, + ); + })), + ), + ); + }), ); } } diff --git a/lib/widgets/friends/friends_list.dart b/lib/widgets/friends/friends_list.dart index b606d1c..4739cc7 100644 --- a/lib/widgets/friends/friends_list.dart +++ b/lib/widgets/friends/friends_list.dart @@ -1,28 +1,10 @@ -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/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'; -import 'package:contacts_plus_plus/widgets/my_profile_dialog.dart'; -import 'package:contacts_plus_plus/widgets/session_list.dart'; -import 'package:contacts_plus_plus/widgets/settings_page.dart'; -import 'package:contacts_plus_plus/widgets/friends/user_search.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; -class MenuItemDefinition { - final String name; - final IconData icon; - final Function() onTap; - - const MenuItemDefinition({required this.name, required this.icon, required this.onTap}); -} class FriendsList extends StatefulWidget { const FriendsList({super.key}); @@ -31,264 +13,78 @@ class FriendsList extends StatefulWidget { State createState() => _FriendsListState(); } -class _FriendsListState extends State { - Future? _userStatusFuture; - ClientHolder? _clientHolder; +class _FriendsListState extends State with AutomaticKeepAliveClientMixin { String _searchFilter = ""; @override - void didChangeDependencies() async { - super.didChangeDependencies(); - final clientHolder = ClientHolder.of(context); - if (_clientHolder != clientHolder) { - _clientHolder = clientHolder; - _refreshUserStatus(); - } - } - - void _refreshUserStatus() { - final apiClient = _clientHolder!.apiClient; - _userStatusFuture = UserApi.getUserStatus(apiClient, userId: apiClient.userId).then((value) async { - if (value.onlineStatus == OnlineStatus.offline) { - final newStatus = value.copyWith( - onlineStatus: - OnlineStatus.values[_clientHolder!.settingsClient.currentSettings.lastOnlineStatus.valueOrDefault]); - await UserApi.setStatus(apiClient, status: newStatus); - return newStatus; - } - return value; - }); + Widget build(BuildContext context) { + super.build(context); + return ChangeNotifierProvider.value( + value: Provider.of(context, listen: false), + child: Stack( + alignment: Alignment.topCenter, + children: [ + Consumer(builder: (context, mClient, _) { + if (mClient.initStatus == null) { + return const LinearProgressIndicator(); + } else if (mClient.initStatus!.isNotEmpty) { + return Column( + children: [ + Expanded( + child: DefaultErrorWidget( + message: mClient.initStatus, + onRetry: () async { + mClient.resetInitStatus(); + mClient.refreshFriendsListWithErrorHandler(); + }, + ), + ), + ], + ); + } else { + var friends = List.from(mClient.cachedFriends); // Explicit copy. + if (_searchFilter.isNotEmpty) { + friends = friends + .where((element) => element.username.toLowerCase().contains(_searchFilter.toLowerCase())) + .toList(); + friends.sort((a, b) => a.username.length.compareTo(b.username.length)); + } + return ListView.builder( + physics: const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast), + itemCount: friends.length, + itemBuilder: (context, index) { + final friend = friends[index]; + final unreads = mClient.getUnreadsForFriend(friend); + return FriendListTile( + friend: friend, + unreads: unreads.length, + ); + }, + ); + } + }), + Align( + alignment: Alignment.bottomCenter, + child: ExpandingInputFab( + onInputChanged: (String text) { + setState(() { + _searchFilter = text; + }); + }, + onExpansionChanged: (expanded) { + if (!expanded) { + setState(() { + _searchFilter = ""; + }); + } + }, + ), + ), + ], + ), + ); } @override - Widget build(BuildContext context) { - final clientHolder = ClientHolder.of(context); - return Scaffold( - appBar: AppBar( - title: const Text("Contacts++"), - actions: [ - FutureBuilder( - future: _userStatusFuture, - builder: (context, snapshot) { - if (snapshot.hasData) { - final userStatus = snapshot.data as UserStatus; - return PopupMenuButton( - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Icon( - Icons.circle, - size: 16, - color: userStatus.onlineStatus.color(context), - ), - ), - Text(toBeginningOfSentenceCase(userStatus.onlineStatus.name) ?? "Unknown"), - ], - ), - onSelected: (OnlineStatus onlineStatus) async { - try { - final newStatus = userStatus.copyWith(onlineStatus: onlineStatus); - setState(() { - _userStatusFuture = Future.value(newStatus.copyWith(lastStatusChange: DateTime.now())); - }); - final settingsClient = ClientHolder.of(context).settingsClient; - await UserApi.setStatus(clientHolder.apiClient, status: newStatus); - await settingsClient.changeSettings( - settingsClient.currentSettings.copyWith(lastOnlineStatus: onlineStatus.index)); - } catch (e, s) { - FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s)); - ScaffoldMessenger.of(context) - .showSnackBar(const SnackBar(content: Text("Failed to set online-status."))); - setState(() { - _userStatusFuture = Future.value(userStatus); - }); - } - }, - itemBuilder: (BuildContext context) => OnlineStatus.values - .where((element) => element == OnlineStatus.online || element == OnlineStatus.invisible) - .map( - (item) => PopupMenuItem( - value: item, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Icon( - Icons.circle, - size: 16, - color: item.color(context), - ), - const SizedBox( - width: 8, - ), - Text(toBeginningOfSentenceCase(item.name)!), - ], - ), - ), - ) - .toList()); - } else if (snapshot.hasError) { - return TextButton.icon( - style: TextButton.styleFrom( - foregroundColor: Theme.of(context).colorScheme.onSurface, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2)), - onPressed: () { - setState(() { - _userStatusFuture = - UserApi.getUserStatus(clientHolder.apiClient, userId: clientHolder.apiClient.userId); - }); - }, - icon: const Icon(Icons.warning), - label: const Text("Retry"), - ); - } else { - return TextButton.icon( - style: TextButton.styleFrom( - disabledForegroundColor: Theme.of(context).colorScheme.onSurface, - ), - onPressed: null, - icon: Container( - width: 16, - height: 16, - margin: const EdgeInsets.only(right: 4), - child: CircularProgressIndicator( - strokeWidth: 2, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - label: const Text("Loading"), - ); - } - }), - Padding( - padding: const EdgeInsets.only(left: 4, right: 4), - child: PopupMenuButton( - icon: const Icon(Icons.more_vert), - onSelected: (MenuItemDefinition itemDef) async { - await itemDef.onTap(); - }, - itemBuilder: (BuildContext context) => [ - MenuItemDefinition( - name: "Settings", - icon: Icons.settings, - onTap: () async { - await Navigator.of(context).push(MaterialPageRoute(builder: (context) => const SettingsPage())); - }, - ), - MenuItemDefinition( - name: "Find Users", - icon: Icons.person_add, - onTap: () async { - final mClient = Provider.of(context, listen: false); - await Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => ChangeNotifierProvider.value( - value: mClient, - child: const UserSearch(), - ), - ), - ); - }, - ), - MenuItemDefinition( - name: "My Profile", - icon: Icons.person, - onTap: () async { - await showDialog( - context: context, - builder: (context) { - return const MyProfileDialog(); - }, - ); - }, - ), - MenuItemDefinition( - name: "Sessions", - icon: Icons.location_city, - onTap: () async { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => const SessionList()), - ); - }, - ), - ].map( - (item) => PopupMenuItem( - value: item, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(item.name), - Icon(item.icon), - ], - ), - ), - ) - .toList(), - ), - ) - ], - ), - body: Stack( - alignment: Alignment.topCenter, - children: [ - Consumer(builder: (context, mClient, _) { - if (mClient.initStatus == null) { - return const LinearProgressIndicator(); - } else if (mClient.initStatus!.isNotEmpty) { - return Column( - children: [ - Expanded( - child: DefaultErrorWidget( - message: mClient.initStatus, - onRetry: () async { - mClient.resetInitStatus(); - mClient.refreshFriendsListWithErrorHandler(); - }, - ), - ), - ], - ); - } else { - var friends = List.from(mClient.cachedFriends); // Explicit copy. - if (_searchFilter.isNotEmpty) { - friends = friends - .where((element) => element.username.toLowerCase().contains(_searchFilter.toLowerCase())) - .toList(); - friends.sort((a, b) => a.username.length.compareTo(b.username.length)); - } - return ListView.builder( - physics: const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast), - itemCount: friends.length, - itemBuilder: (context, index) { - final friend = friends[index]; - final unreads = mClient.getUnreadsForFriend(friend); - return FriendListTile( - friend: friend, - unreads: unreads.length, - ); - }, - ); - } - }), - Align( - alignment: Alignment.bottomCenter, - child: ExpandingInputFab( - onInputChanged: (String text) { - setState(() { - _searchFilter = text; - }); - }, - onExpansionChanged: (expanded) { - if (!expanded) { - setState(() { - _searchFilter = ""; - }); - } - }, - ), - ), - ], - ), - ); - } + bool get wantKeepAlive => true; } diff --git a/lib/widgets/friends/friends_list_app_bar.dart b/lib/widgets/friends/friends_list_app_bar.dart new file mode 100644 index 0000000..020eff8 --- /dev/null +++ b/lib/widgets/friends/friends_list_app_bar.dart @@ -0,0 +1,223 @@ +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/users/online_status.dart'; +import 'package:contacts_plus_plus/models/users/user_status.dart'; +import 'package:contacts_plus_plus/widgets/friends/user_search.dart'; +import 'package:contacts_plus_plus/widgets/my_profile_dialog.dart'; +import 'package:contacts_plus_plus/widgets/settings_page.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +class FriendsListAppBar extends StatefulWidget { + const FriendsListAppBar({super.key}); + + @override + State createState() => _FriendsListAppBarState(); +} + +class _FriendsListAppBarState extends State with AutomaticKeepAliveClientMixin { + Future? _userStatusFuture; + ClientHolder? _clientHolder; + + @override + void didChangeDependencies() async { + super.didChangeDependencies(); + final clientHolder = ClientHolder.of(context); + if (_clientHolder != clientHolder) { + _clientHolder = clientHolder; + _refreshUserStatus(); + } + } + + void _refreshUserStatus() { + final apiClient = _clientHolder!.apiClient; + _userStatusFuture ??= UserApi.getUserStatus(apiClient, userId: apiClient.userId).then((value) async { + if (value.onlineStatus == OnlineStatus.offline) { + final newStatus = value.copyWith( + onlineStatus: + OnlineStatus.values[_clientHolder!.settingsClient.currentSettings.lastOnlineStatus.valueOrDefault]); + await UserApi.setStatus(apiClient, status: newStatus); + return newStatus; + } + return value; + }); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return ChangeNotifierProvider.value( + value: Provider.of(context, listen: false), + child: AppBar( + title: const Text("Contacts++"), + actions: [ + FutureBuilder( + future: _userStatusFuture, + builder: (context, snapshot) { + if (snapshot.hasData) { + final userStatus = snapshot.data as UserStatus; + return PopupMenuButton( + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Icon( + Icons.circle, + size: 16, + color: userStatus.onlineStatus.color(context), + ), + ), + Text(toBeginningOfSentenceCase(userStatus.onlineStatus.name) ?? "Unknown"), + ], + ), + onSelected: (OnlineStatus onlineStatus) async { + try { + final newStatus = userStatus.copyWith(onlineStatus: onlineStatus); + setState(() { + _userStatusFuture = Future.value(newStatus.copyWith(lastStatusChange: DateTime.now())); + }); + final settingsClient = _clientHolder!.settingsClient; + await UserApi.setStatus(_clientHolder!.apiClient, status: newStatus); + await settingsClient.changeSettings( + settingsClient.currentSettings.copyWith(lastOnlineStatus: onlineStatus.index)); + } catch (e, s) { + FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s)); + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text("Failed to set online-status."))); + setState(() { + _userStatusFuture = Future.value(userStatus); + }); + } + }, + itemBuilder: (BuildContext context) => OnlineStatus.values + .where((element) => element == OnlineStatus.online || element == OnlineStatus.invisible) + .map( + (item) => PopupMenuItem( + value: item, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Icon( + Icons.circle, + size: 16, + color: item.color(context), + ), + const SizedBox( + width: 8, + ), + Text(toBeginningOfSentenceCase(item.name)!), + ], + ), + ), + ) + .toList()); + } else if (snapshot.hasError) { + return TextButton.icon( + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.onSurface, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2)), + onPressed: () { + setState(() { + _userStatusFuture = + UserApi.getUserStatus(_clientHolder!.apiClient, userId: _clientHolder!.apiClient.userId); + }); + }, + icon: const Icon(Icons.warning), + label: const Text("Retry"), + ); + } else { + return TextButton.icon( + style: TextButton.styleFrom( + disabledForegroundColor: Theme.of(context).colorScheme.onSurface, + ), + onPressed: null, + icon: Container( + width: 16, + height: 16, + margin: const EdgeInsets.only(right: 4), + child: CircularProgressIndicator( + strokeWidth: 2, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + label: const Text("Loading"), + ); + } + }, + ), + Padding( + padding: const EdgeInsets.only(left: 4, right: 4), + child: PopupMenuButton( + icon: const Icon(Icons.more_vert), + onSelected: (MenuItemDefinition itemDef) async { + await itemDef.onTap(); + }, + itemBuilder: (BuildContext context) => [ + MenuItemDefinition( + name: "Settings", + icon: Icons.settings, + onTap: () async { + await Navigator.of(context).push(MaterialPageRoute(builder: (context) => const SettingsPage())); + }, + ), + MenuItemDefinition( + name: "Find Users", + icon: Icons.person_add, + onTap: () async { + final mClient = Provider.of(context, listen: false); + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ChangeNotifierProvider.value( + value: mClient, + child: const UserSearch(), + ), + ), + ); + }, + ), + MenuItemDefinition( + name: "My Profile", + icon: Icons.person, + onTap: () async { + await showDialog( + context: context, + builder: (context) { + return const MyProfileDialog(); + }, + ); + }, + ), + ] + .map( + (item) => PopupMenuItem( + value: item, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(item.name), + Icon(item.icon), + ], + ), + ), + ) + .toList(), + ), + ) + ], + ), + ); + } + + @override + bool get wantKeepAlive => true; +} + +class MenuItemDefinition { + final String name; + final IconData icon; + final Function() onTap; + + const MenuItemDefinition({required this.name, required this.icon, required this.onTap}); +} diff --git a/lib/widgets/global_app_bar.dart b/lib/widgets/global_app_bar.dart new file mode 100644 index 0000000..74dbb15 --- /dev/null +++ b/lib/widgets/global_app_bar.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class GlobalAppBar extends StatefulWidget implements PreferredSizeWidget { + const GlobalAppBar({super.key}); + + @override + State createState() => _GlobalAppBarState(); + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} + +class _GlobalAppBarState extends State { + @override + Widget build(BuildContext context) { + return AppBar( + + ); + } +} \ No newline at end of file diff --git a/lib/widgets/messages/message_session_invite.dart b/lib/widgets/messages/message_session_invite.dart index 0eb0a56..ee52cb2 100644 --- a/lib/widgets/messages/message_session_invite.dart +++ b/lib/widgets/messages/message_session_invite.dart @@ -5,7 +5,6 @@ import 'package:contacts_plus_plus/models/message.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/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'; diff --git a/lib/widgets/session_list.dart b/lib/widgets/session_list.dart index 3f343a8..157301b 100644 --- a/lib/widgets/session_list.dart +++ b/lib/widgets/session_list.dart @@ -1,4 +1,5 @@ import 'package:cached_network_image/cached_network_image.dart'; +import 'package:collection/collection.dart'; import 'package:contacts_plus_plus/apis/session_api.dart'; import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:contacts_plus_plus/client_holder.dart'; @@ -14,146 +15,133 @@ class SessionList extends StatefulWidget { State createState() => _SessionListState(); } -class _SessionListState extends State { +class _SessionListState extends State with AutomaticKeepAliveClientMixin { Future>? _sessionsFuture; @override void didChangeDependencies() { super.didChangeDependencies(); - _sessionsFuture = SessionApi.getSessions(ClientHolder.of(context).apiClient); + _sessionsFuture ??= SessionApi.getSessions(ClientHolder.of(context).apiClient).then( + (value) => value.sorted( + (a, b) => b.sessionUsers.length.compareTo(a.sessionUsers.length), + ), + ); } @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - scrolledUnderElevation: 0, - title: const Text("Sessions"), - backgroundColor: Theme.of(context).colorScheme.surfaceVariant, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1), - child: Row( - children: [ - Expanded( - child: Container( - width: double.infinity, - height: 1, - color: Colors.black, + super.build(context); + return FutureBuilder>( + future: _sessionsFuture, + builder: (context, snapshot) { + final data = snapshot.data ?? []; + return Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: GridView.builder( + padding: const EdgeInsets.only(top: 10), + physics: const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast), + itemCount: data.length, + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 256, + crossAxisSpacing: 4, + mainAxisSpacing: 4, + childAspectRatio: .8, ), - ), - ], - ), - ), - ), - body: FutureBuilder>( - future: _sessionsFuture, - builder: (context, snapshot) { - final data = snapshot.data ?? []; - return Stack( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: GridView.builder( - padding: const EdgeInsets.only(top: 10), - physics: const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast), - itemCount: data.length, - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 256, - crossAxisSpacing: 4, - mainAxisSpacing: 4, - childAspectRatio: .8, - ), - itemBuilder: (context, index) { - final session = data[index]; - return Card( - elevation: 0, - shape: RoundedRectangleBorder( - side: BorderSide( - color: Theme.of(context).colorScheme.outline, - ), - borderRadius: BorderRadius.circular(16), + itemBuilder: (context, index) { + final session = data[index]; + return Card( + elevation: 0, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).colorScheme.outline, ), - child: InkWell( - onTap: () { - Navigator.of(context) - .push(MaterialPageRoute(builder: (context) => SessionView(session: session))); - }, - borderRadius: BorderRadius.circular(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 5, - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: Hero( - tag: session.id, - child: CachedNetworkImage( - imageUrl: Aux.neosDbToHttp(session.thumbnail), - fit: BoxFit.cover, - errorWidget: (context, url, error) => const Center( - child: Icon( - Icons.broken_image, - size: 64, - ), + borderRadius: BorderRadius.circular(16), + ), + child: InkWell( + onTap: () { + Navigator.of(context) + .push(MaterialPageRoute(builder: (context) => SessionView(session: session))); + }, + borderRadius: BorderRadius.circular(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 5, + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Hero( + tag: session.id, + child: CachedNetworkImage( + imageUrl: Aux.neosDbToHttp(session.thumbnail), + fit: BoxFit.cover, + errorWidget: (context, url, error) => const Center( + child: Icon( + Icons.broken_image, + size: 64, ), - placeholder: (context, uri) => const Center(child: CircularProgressIndicator()), ), + placeholder: (context, uri) => const Center(child: CircularProgressIndicator()), ), ), ), - Expanded( - flex: 2, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: FormattedText( - session.formattedName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + ), + Expanded( + flex: 2, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: FormattedText( + session.formattedName, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - ], - ), - const SizedBox( - height: 4, - ), - Row( - children: [ - Expanded( - child: Text( - "${session.sessionUsers.length.toString().padLeft(2, "0")}/${session.maxUsers.toString().padLeft(2, "0")} Online", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(.5), - ), - ), + ), + ], + ), + const SizedBox( + height: 4, + ), + Row( + children: [ + Expanded( + child: Text( + "${session.sessionUsers.length.toString().padLeft(2, "0")}/${session.maxUsers.toString().padLeft(2, "0")} Online", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(.5), + ), ), - ], - ), - ], - ), + ), + ], + ), + ], ), - ) - ], - ), + ), + ) + ], ), - ); - }, - ), + ), + ); + }, ), - if (snapshot.connectionState == ConnectionState.waiting) const LinearProgressIndicator() - ], - ); - }, - ), + ), + if (snapshot.connectionState == ConnectionState.waiting) const LinearProgressIndicator() + ], + ); + }, ); } + + @override + bool get wantKeepAlive => true; } diff --git a/lib/widgets/session_list_app_bar.dart b/lib/widgets/session_list_app_bar.dart new file mode 100644 index 0000000..3175049 --- /dev/null +++ b/lib/widgets/session_list_app_bar.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class SessionListAppBar extends StatelessWidget { + const SessionListAppBar({super.key}); + + @override + Widget build(BuildContext context) { + return AppBar( + title: const Text("Sessions"), + actions: [ + IconButton( + onPressed: () { + + }, + icon: const Icon(Icons.filter_alt_outlined), + ) + ], + ); + } +} diff --git a/lib/widgets/settings_app_bar.dart b/lib/widgets/settings_app_bar.dart new file mode 100644 index 0000000..5433939 --- /dev/null +++ b/lib/widgets/settings_app_bar.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class SettingsAppBar extends StatelessWidget { + const SettingsAppBar({super.key}); + + @override + Widget build(BuildContext context) { + return AppBar( + title: const Text("Settings"), + ); + } + +} \ No newline at end of file diff --git a/lib/widgets/settings_page.dart b/lib/widgets/settings_page.dart index f72ab9f..28e0895 100644 --- a/lib/widgets/settings_page.dart +++ b/lib/widgets/settings_page.dart @@ -11,17 +11,7 @@ class SettingsPage extends StatelessWidget { @override Widget build(BuildContext context) { final sClient = ClientHolder.of(context).settingsClient; - return Scaffold( - appBar: AppBar( - leading: IconButton( - onPressed: () { - Navigator.of(context).pop(); - }, - icon: const Icon(Icons.arrow_back), - ), - title: const Text("Settings"), - ), - body: ListView( + return ListView( children: [ const ListSectionHeader(leadingText: "Notifications"), BooleanSettingsTile( @@ -110,8 +100,7 @@ class SettingsPage extends StatelessWidget { }, ) ], - ), - ); + ); } }