Add tabbed view to homescreen

This commit is contained in:
Nutcake 2023-06-03 17:17:54 +02:00
parent e8ea2c9797
commit 24178aaa43
10 changed files with 576 additions and 463 deletions

View file

@ -68,6 +68,7 @@ class MessagingClient extends ChangeNotifier {
MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient}) MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient})
: _apiClient = apiClient, _notificationClient = notificationClient { : _apiClient = apiClient, _notificationClient = notificationClient {
debugPrint("mClient created: $hashCode");
Hive.openBox(_messageBoxKey).then((box) async { Hive.openBox(_messageBoxKey).then((box) async {
box.delete(_lastUpdateKey); box.delete(_lastUpdateKey);
await refreshFriendsListWithErrorHandler(); await refreshFriendsListWithErrorHandler();
@ -84,6 +85,7 @@ class MessagingClient extends ChangeNotifier {
@override @override
void dispose() { void dispose() {
debugPrint("mClient disposed: $hashCode");
_autoRefresh?.cancel(); _autoRefresh?.cancel();
_notifyOnlineTimer?.cancel(); _notifyOnlineTimer?.cancel();
_unreadSafeguard?.cancel(); _unreadSafeguard?.cancel();

View file

@ -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/clients/settings_client.dart';
import 'package:contacts_plus_plus/models/sem_ver.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.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/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:contacts_plus_plus/widgets/update_notifier.dart';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -22,12 +27,15 @@ import 'models/authentication_data.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
Provider.debugCheckInvalidValueType = null;
await Hive.initFlutter(); await Hive.initFlutter();
final dateFormat = DateFormat.Hms(); 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(); final settingsClient = SettingsClient();
await settingsClient.loadSettings(); 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 await settingsClient.changeSettings(newSettings); // Save generated machineId to disk
AuthenticationData cachedAuth = AuthenticationData.unauthenticated(); AuthenticationData cachedAuth = AuthenticationData.unauthenticated();
try { try {
@ -47,15 +55,28 @@ class ContactsPlusPlus extends StatefulWidget {
} }
class _ContactsPlusPlusState extends State<ContactsPlusPlus> { class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
static const List<Widget> _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 Typography _typography = Typography.material2021(platform: TargetPlatform.android);
final PageController _pageController = PageController();
late AuthenticationData _authData = widget.cachedAuthentication; late AuthenticationData _authData = widget.cachedAuthentication;
bool _checkedForUpdate = false; bool _checkedForUpdate = false;
int _selectedPage = 0;
void showUpdateDialogOnFirstBuild(BuildContext context) { void showUpdateDialogOnFirstBuild(BuildContext context) {
final navigator = Navigator.of(context); final navigator = Navigator.of(context);
final settings = ClientHolder final settings = ClientHolder.of(context).settingsClient;
.of(context)
.settingsClient;
if (_checkedForUpdate) return; if (_checkedForUpdate) return;
_checkedForUpdate = true; _checkedForUpdate = true;
GithubApi.getLatestTagName().then((remoteVer) async { GithubApi.getLatestTagName().then((remoteVer) async {
@ -103,61 +124,103 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Phoenix( return Phoenix(
child: Builder( child: Builder(builder: (context) {
builder: (context) { return ClientHolder(
return ClientHolder( settingsClient: widget.settingsClient,
settingsClient: widget.settingsClient, authenticationData: _authData,
authenticationData: _authData, onLogout: () {
onLogout: () { setState(() {
setState(() { _authData = AuthenticationData.unauthenticated();
_authData = AuthenticationData.unauthenticated(); });
}); Phoenix.rebirth(context);
Phoenix.rebirth(context); },
}, child: DynamicColorBuilder(
child: DynamicColorBuilder( builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) => MaterialApp(
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) => MaterialApp( debugShowCheckedModeBanner: false,
debugShowCheckedModeBanner: false, title: 'Contacts++',
title: 'Contacts++', theme: ThemeData(
theme: ThemeData( useMaterial3: true,
useMaterial3: true, textTheme: _typography.black,
textTheme: _typography.black, colorScheme:
colorScheme: lightDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.light), lightDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.light),
), ),
darkTheme: ThemeData( darkTheme: ThemeData(
useMaterial3: true, useMaterial3: true,
textTheme: _typography.white, textTheme: _typography.white,
colorScheme: darkDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark), 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 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) { builder: (context) {
showUpdateDialogOnFirstBuild(context); showUpdateDialogOnFirstBuild(context);
final clientHolder = ClientHolder.of(context); final clientHolder = ClientHolder.of(context);
return _authData.isAuthenticated ? return _authData.isAuthenticated
ChangeNotifierProvider( // This doesn't need to be a proxy provider since the arguments should never change during it's lifetime. ? Provider(
create: (context) => create: (context) => MessagingClient(
MessagingClient( apiClient: clientHolder.apiClient,
apiClient: clientHolder.apiClient, notificationClient: clientHolder.notificationClient,
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(), body: PageView(
) : controller: _pageController,
LoginScreen( children: const [
onLoginSuccessful: (AuthenticationData authData) async { FriendsList(),
if (authData.isAuthenticated) { SessionList(),
setState(() { SettingsPage(),
_authData = authData; ],
}); ),
} 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;
});
}
},
);
})),
),
);
}),
); );
} }
} }

View file

@ -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/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/default_error_widget.dart';
import 'package:contacts_plus_plus/widgets/friends/expanding_input_fab.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/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:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.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 { class FriendsList extends StatefulWidget {
const FriendsList({super.key}); const FriendsList({super.key});
@ -31,264 +13,78 @@ class FriendsList extends StatefulWidget {
State<FriendsList> createState() => _FriendsListState(); State<FriendsList> createState() => _FriendsListState();
} }
class _FriendsListState extends State<FriendsList> { class _FriendsListState extends State<FriendsList> with AutomaticKeepAliveClientMixin {
Future<UserStatus>? _userStatusFuture;
ClientHolder? _clientHolder;
String _searchFilter = ""; String _searchFilter = "";
@override @override
void didChangeDependencies() async { Widget build(BuildContext context) {
super.didChangeDependencies(); super.build(context);
final clientHolder = ClientHolder.of(context); return ChangeNotifierProvider.value(
if (_clientHolder != clientHolder) { value: Provider.of<MessagingClient>(context, listen: false),
_clientHolder = clientHolder; child: Stack(
_refreshUserStatus(); alignment: Alignment.topCenter,
} children: [
} Consumer<MessagingClient>(builder: (context, mClient, _) {
if (mClient.initStatus == null) {
void _refreshUserStatus() { return const LinearProgressIndicator();
final apiClient = _clientHolder!.apiClient; } else if (mClient.initStatus!.isNotEmpty) {
_userStatusFuture = UserApi.getUserStatus(apiClient, userId: apiClient.userId).then((value) async { return Column(
if (value.onlineStatus == OnlineStatus.offline) { children: [
final newStatus = value.copyWith( Expanded(
onlineStatus: child: DefaultErrorWidget(
OnlineStatus.values[_clientHolder!.settingsClient.currentSettings.lastOnlineStatus.valueOrDefault]); message: mClient.initStatus,
await UserApi.setStatus(apiClient, status: newStatus); onRetry: () async {
return newStatus; mClient.resetInitStatus();
} mClient.refreshFriendsListWithErrorHandler();
return value; },
}); ),
),
],
);
} 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 @override
Widget build(BuildContext context) { bool get wantKeepAlive => true;
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<OnlineStatus>(
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<OnlineStatus>(
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<MenuItemDefinition>(
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<MessagingClient>(context, listen: false);
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider<MessagingClient>.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<MenuItemDefinition>(
value: item,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(item.name),
Icon(item.icon),
],
),
),
)
.toList(),
),
)
],
),
body: Stack(
alignment: Alignment.topCenter,
children: [
Consumer<MessagingClient>(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 = "";
});
}
},
),
),
],
),
);
}
} }

View file

@ -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<StatefulWidget> createState() => _FriendsListAppBarState();
}
class _FriendsListAppBarState extends State<FriendsListAppBar> with AutomaticKeepAliveClientMixin {
Future<UserStatus>? _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<MessagingClient>(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<OnlineStatus>(
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<OnlineStatus>(
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<MenuItemDefinition>(
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<MessagingClient>(context, listen: false);
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider<MessagingClient>.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<MenuItemDefinition>(
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});
}

View file

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
class GlobalAppBar extends StatefulWidget implements PreferredSizeWidget {
const GlobalAppBar({super.key});
@override
State<StatefulWidget> createState() => _GlobalAppBarState();
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
class _GlobalAppBarState extends State<GlobalAppBar> {
@override
Widget build(BuildContext context) {
return AppBar(
);
}
}

View file

@ -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/models/session.dart';
import 'package:contacts_plus_plus/widgets/formatted_text.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/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/messages/message_state_indicator.dart';
import 'package:contacts_plus_plus/widgets/session_view.dart'; import 'package:contacts_plus_plus/widgets/session_view.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View file

@ -1,4 +1,5 @@
import 'package:cached_network_image/cached_network_image.dart'; 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/apis/session_api.dart';
import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:contacts_plus_plus/auxiliary.dart';
import 'package:contacts_plus_plus/client_holder.dart'; import 'package:contacts_plus_plus/client_holder.dart';
@ -14,146 +15,133 @@ class SessionList extends StatefulWidget {
State<SessionList> createState() => _SessionListState(); State<SessionList> createState() => _SessionListState();
} }
class _SessionListState extends State<SessionList> { class _SessionListState extends State<SessionList> with AutomaticKeepAliveClientMixin {
Future<List<Session>>? _sessionsFuture; Future<List<Session>>? _sessionsFuture;
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( super.build(context);
appBar: AppBar( return FutureBuilder<List<Session>>(
scrolledUnderElevation: 0, future: _sessionsFuture,
title: const Text("Sessions"), builder: (context, snapshot) {
backgroundColor: Theme.of(context).colorScheme.surfaceVariant, final data = snapshot.data ?? [];
bottom: PreferredSize( return Stack(
preferredSize: const Size.fromHeight(1), children: [
child: Row( Padding(
children: [ padding: const EdgeInsets.symmetric(horizontal: 8.0),
Expanded( child: GridView.builder(
child: Container( padding: const EdgeInsets.only(top: 10),
width: double.infinity, physics: const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast),
height: 1, itemCount: data.length,
color: Colors.black, gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 256,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
childAspectRatio: .8,
), ),
), itemBuilder: (context, index) {
], final session = data[index];
), return Card(
), elevation: 0,
), shape: RoundedRectangleBorder(
body: FutureBuilder<List<Session>>( side: BorderSide(
future: _sessionsFuture, color: Theme.of(context).colorScheme.outline,
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),
), ),
child: InkWell( borderRadius: BorderRadius.circular(16),
onTap: () { ),
Navigator.of(context) child: InkWell(
.push(MaterialPageRoute(builder: (context) => SessionView(session: session))); onTap: () {
}, Navigator.of(context)
borderRadius: BorderRadius.circular(16), .push(MaterialPageRoute(builder: (context) => SessionView(session: session)));
child: Column( },
crossAxisAlignment: CrossAxisAlignment.start, borderRadius: BorderRadius.circular(16),
children: [ child: Column(
Expanded( crossAxisAlignment: CrossAxisAlignment.start,
flex: 5, children: [
child: ClipRRect( Expanded(
borderRadius: BorderRadius.circular(16), flex: 5,
child: Hero( child: ClipRRect(
tag: session.id, borderRadius: BorderRadius.circular(16),
child: CachedNetworkImage( child: Hero(
imageUrl: Aux.neosDbToHttp(session.thumbnail), tag: session.id,
fit: BoxFit.cover, child: CachedNetworkImage(
errorWidget: (context, url, error) => const Center( imageUrl: Aux.neosDbToHttp(session.thumbnail),
child: Icon( fit: BoxFit.cover,
Icons.broken_image, errorWidget: (context, url, error) => const Center(
size: 64, child: Icon(
), Icons.broken_image,
size: 64,
), ),
placeholder: (context, uri) => const Center(child: CircularProgressIndicator()),
), ),
placeholder: (context, uri) => const Center(child: CircularProgressIndicator()),
), ),
), ),
), ),
Expanded( ),
flex: 2, Expanded(
child: Padding( flex: 2,
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16), child: Padding(
child: Column( padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16),
mainAxisAlignment: MainAxisAlignment.center, child: Column(
crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center,
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Row( children: [
children: [ Row(
Expanded( children: [
child: FormattedText( Expanded(
session.formattedName, child: FormattedText(
maxLines: 1, session.formattedName,
overflow: TextOverflow.ellipsis, maxLines: 1,
), overflow: TextOverflow.ellipsis,
), ),
], ),
), ],
const SizedBox( ),
height: 4, const SizedBox(
), height: 4,
Row( ),
children: [ Row(
Expanded( children: [
child: Text( Expanded(
"${session.sessionUsers.length.toString().padLeft(2, "0")}/${session.maxUsers.toString().padLeft(2, "0")} Online", child: Text(
maxLines: 1, "${session.sessionUsers.length.toString().padLeft(2, "0")}/${session.maxUsers.toString().padLeft(2, "0")} Online",
overflow: TextOverflow.ellipsis, maxLines: 1,
style: Theme.of(context).textTheme.bodySmall?.copyWith( overflow: TextOverflow.ellipsis,
color: Theme.of(context).colorScheme.onSurface.withOpacity(.5), 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;
} }

View file

@ -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),
)
],
);
}
}

View file

@ -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"),
);
}
}

View file

@ -11,17 +11,7 @@ class SettingsPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sClient = ClientHolder.of(context).settingsClient; final sClient = ClientHolder.of(context).settingsClient;
return Scaffold( return ListView(
appBar: AppBar(
leading: IconButton(
onPressed: () {
Navigator.of(context).pop();
},
icon: const Icon(Icons.arrow_back),
),
title: const Text("Settings"),
),
body: ListView(
children: [ children: [
const ListSectionHeader(leadingText: "Notifications"), const ListSectionHeader(leadingText: "Notifications"),
BooleanSettingsTile( BooleanSettingsTile(
@ -110,8 +100,7 @@ class SettingsPage extends StatelessWidget {
}, },
) )
], ],
), );
);
} }
} }