diff --git a/lib/apis/session_api.dart b/lib/apis/session_api.dart index fab8d1c..952e45d 100644 --- a/lib/apis/session_api.dart +++ b/lib/apis/session_api.dart @@ -10,4 +10,11 @@ class SessionApi { final body = jsonDecode(response.body); return Session.fromMap(body); } + + static Future> getSessions(ApiClient client) async { + final response = await client.get("/sessions"); + client.checkResponse(response); + final body = jsonDecode(response.body) as List; + return body.map((e) => Session.fromMap(e)).toList(); + } } \ No newline at end of file diff --git a/lib/widgets/friends/friends_list.dart b/lib/widgets/friends/friends_list.dart index 8549fcb..b606d1c 100644 --- a/lib/widgets/friends/friends_list.dart +++ b/lib/widgets/friends/friends_list.dart @@ -9,13 +9,13 @@ 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; @@ -51,9 +51,8 @@ class _FriendsListState extends State { _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] - ); + onlineStatus: + OnlineStatus.values[_clientHolder!.settingsClient.currentSettings.lastOnlineStatus.valueOrDefault]); await UserApi.setStatus(apiClient, status: newStatus); return newStatus; } @@ -78,7 +77,11 @@ class _FriendsListState extends State { children: [ Padding( padding: const EdgeInsets.only(right: 8.0), - child: Icon(Icons.circle, size: 16, color: userStatus.onlineStatus.color(context),), + child: Icon( + Icons.circle, + size: 16, + color: userStatus.onlineStatus.color(context), + ), ), Text(toBeginningOfSentenceCase(userStatus.onlineStatus.name) ?? "Unknown"), ], @@ -89,50 +92,50 @@ class _FriendsListState extends State { setState(() { _userStatusFuture = Future.value(newStatus.copyWith(lastStatusChange: DateTime.now())); }); - final settingsClient = ClientHolder - .of(context) - .settingsClient; + 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."))); + 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)!), - ], - ), + 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()); + ), + ) + .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) - ), + 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); + _userStatusFuture = + UserApi.getUserStatus(clientHolder.apiClient, userId: clientHolder.apiClient.userId); }); }, icon: const Icon(Icons.warning), @@ -141,10 +144,7 @@ class _FriendsListState extends State { } else { return TextButton.icon( style: TextButton.styleFrom( - disabledForegroundColor: Theme - .of(context) - .colorScheme - .onSurface, + disabledForegroundColor: Theme.of(context).colorScheme.onSurface, ), onPressed: null, icon: Container( @@ -153,17 +153,13 @@ class _FriendsListState extends State { margin: const EdgeInsets.only(right: 4), child: CircularProgressIndicator( strokeWidth: 2, - color: Theme - .of(context) - .colorScheme - .onSurface, + color: Theme.of(context).colorScheme.onSurface, ), ), label: const Text("Loading"), ); } - } - ), + }), Padding( padding: const EdgeInsets.only(left: 4, right: 4), child: PopupMenuButton( @@ -171,55 +167,63 @@ class _FriendsListState extends State { 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), - ], + 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(), ), ), - ).toList(), + ); + }, + ), + 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(), ), ) ], @@ -227,46 +231,45 @@ class _FriendsListState extends State { 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, - ); - }, - ); - } + 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( @@ -288,4 +291,4 @@ class _FriendsListState extends State { ), ); } -} \ No newline at end of file +} diff --git a/lib/widgets/session_list.dart b/lib/widgets/session_list.dart new file mode 100644 index 0000000..3f343a8 --- /dev/null +++ b/lib/widgets/session_list.dart @@ -0,0 +1,159 @@ +import 'package:cached_network_image/cached_network_image.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'; +import 'package:contacts_plus_plus/models/session.dart'; +import 'package:contacts_plus_plus/widgets/formatted_text.dart'; +import 'package:contacts_plus_plus/widgets/session_view.dart'; +import 'package:flutter/material.dart'; + +class SessionList extends StatefulWidget { + const SessionList({super.key}); + + @override + State createState() => _SessionListState(); +} + +class _SessionListState extends State { + Future>? _sessionsFuture; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _sessionsFuture = SessionApi.getSessions(ClientHolder.of(context).apiClient); + } + + @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, + ), + ), + ], + ), + ), + ), + 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), + ), + 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()), + ), + ), + ), + ), + 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), + ), + ), + ), + ], + ), + ], + ), + ), + ) + ], + ), + ), + ); + }, + ), + ), + if (snapshot.connectionState == ConnectionState.waiting) const LinearProgressIndicator() + ], + ); + }, + ), + ); + } +} diff --git a/lib/widgets/session_view.dart b/lib/widgets/session_view.dart index a31447a..cc053aa 100644 --- a/lib/widgets/session_view.dart +++ b/lib/widgets/session_view.dart @@ -52,36 +52,38 @@ class SessionView extends StatelessWidget { ), 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), - ), + background: Hero( + tag: session.id, + child: CachedNetworkImage( + imageUrl: Aux.neosDbToHttp(session.thumbnail), + imageBuilder: (context, image) { + return Material( + child: InkWell( + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PhotoView( + minScale: PhotoViewComputedScale.contained, + imageProvider: image, + heroAttributes: PhotoViewHeroAttributes(tag: session.id), + ), + ), + ); + }, + child: Image( + image: image, + fit: BoxFit.cover, ), - ); - }, - child: Hero( - tag: session.id, - child: Image( - image: image, - fit: BoxFit.cover, ), - ), - ); - }, - errorWidget: (context, url, error) => const Icon( - Icons.broken_image, - size: 64, + ); + }, + errorWidget: (context, url, error) => const Icon( + Icons.broken_image, + size: 64, + ), + placeholder: (context, uri) => const Center(child: CircularProgressIndicator()), ), - placeholder: (context, uri) => const Center(child: CircularProgressIndicator()), ), ), ), @@ -162,7 +164,7 @@ class SessionView extends StatelessWidget { SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { - final user = session.sessionUsers[index % session.sessionUsers.length]; + final user = session.sessionUsers[index]; return ListTile( dense: true, title: Text( @@ -175,7 +177,7 @@ class SessionView extends StatelessWidget { ), ); }, - childCount: session.sessionUsers.length * 4, + childCount: session.sessionUsers.length, ), ) ],