Add barebones session list viewer

This commit is contained in:
Nutcake 2023-05-30 15:09:38 +02:00
parent a7f39c1d06
commit e8ea2c9797
4 changed files with 330 additions and 159 deletions

View file

@ -10,4 +10,11 @@ class SessionApi {
final body = jsonDecode(response.body); final body = jsonDecode(response.body);
return Session.fromMap(body); return Session.fromMap(body);
} }
static Future<List<Session>> 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();
}
} }

View file

@ -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/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/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/settings_page.dart';
import 'package:contacts_plus_plus/widgets/friends/user_search.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:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class MenuItemDefinition { class MenuItemDefinition {
final String name; final String name;
final IconData icon; final IconData icon;
@ -51,9 +51,8 @@ class _FriendsListState extends State<FriendsList> {
_userStatusFuture = UserApi.getUserStatus(apiClient, userId: apiClient.userId).then((value) async { _userStatusFuture = UserApi.getUserStatus(apiClient, userId: apiClient.userId).then((value) async {
if (value.onlineStatus == OnlineStatus.offline) { if (value.onlineStatus == OnlineStatus.offline) {
final newStatus = value.copyWith( final newStatus = value.copyWith(
onlineStatus: OnlineStatus.values[_clientHolder!.settingsClient.currentSettings.lastOnlineStatus onlineStatus:
.valueOrDefault] OnlineStatus.values[_clientHolder!.settingsClient.currentSettings.lastOnlineStatus.valueOrDefault]);
);
await UserApi.setStatus(apiClient, status: newStatus); await UserApi.setStatus(apiClient, status: newStatus);
return newStatus; return newStatus;
} }
@ -78,7 +77,11 @@ class _FriendsListState extends State<FriendsList> {
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(right: 8.0), 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"), Text(toBeginningOfSentenceCase(userStatus.onlineStatus.name) ?? "Unknown"),
], ],
@ -89,50 +92,50 @@ class _FriendsListState extends State<FriendsList> {
setState(() { setState(() {
_userStatusFuture = Future.value(newStatus.copyWith(lastStatusChange: DateTime.now())); _userStatusFuture = Future.value(newStatus.copyWith(lastStatusChange: DateTime.now()));
}); });
final settingsClient = ClientHolder final settingsClient = ClientHolder.of(context).settingsClient;
.of(context)
.settingsClient;
await UserApi.setStatus(clientHolder.apiClient, status: newStatus); await UserApi.setStatus(clientHolder.apiClient, status: newStatus);
await settingsClient.changeSettings( await settingsClient.changeSettings(
settingsClient.currentSettings.copyWith(lastOnlineStatus: onlineStatus.index)); settingsClient.currentSettings.copyWith(lastOnlineStatus: onlineStatus.index));
} catch (e, s) { } catch (e, s) {
FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s)); FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s));
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text( ScaffoldMessenger.of(context)
"Failed to set online-status."))); .showSnackBar(const SnackBar(content: Text("Failed to set online-status.")));
setState(() { setState(() {
_userStatusFuture = Future.value(userStatus); _userStatusFuture = Future.value(userStatus);
}); });
} }
}, },
itemBuilder: (BuildContext context) => itemBuilder: (BuildContext context) => OnlineStatus.values
OnlineStatus.values.where((element) => .where((element) => element == OnlineStatus.online || element == OnlineStatus.invisible)
element == OnlineStatus.online .map(
|| element == OnlineStatus.invisible).map((item) => (item) => PopupMenuItem<OnlineStatus>(
PopupMenuItem<OnlineStatus>(
value: item, value: item,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
Icon(Icons.circle, size: 16, color: item.color(context),), Icon(
const SizedBox(width: 8,), Icons.circle,
size: 16,
color: item.color(context),
),
const SizedBox(
width: 8,
),
Text(toBeginningOfSentenceCase(item.name)!), Text(toBeginningOfSentenceCase(item.name)!),
], ],
), ),
), ),
).toList()); )
.toList());
} else if (snapshot.hasError) { } else if (snapshot.hasError) {
return TextButton.icon( return TextButton.icon(
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: Theme foregroundColor: Theme.of(context).colorScheme.onSurface,
.of(context) padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2)),
.colorScheme
.onSurface,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2)
),
onPressed: () { onPressed: () {
setState(() { setState(() {
_userStatusFuture = UserApi.getUserStatus(clientHolder.apiClient, userId: clientHolder.apiClient _userStatusFuture =
.userId); UserApi.getUserStatus(clientHolder.apiClient, userId: clientHolder.apiClient.userId);
}); });
}, },
icon: const Icon(Icons.warning), icon: const Icon(Icons.warning),
@ -141,10 +144,7 @@ class _FriendsListState extends State<FriendsList> {
} else { } else {
return TextButton.icon( return TextButton.icon(
style: TextButton.styleFrom( style: TextButton.styleFrom(
disabledForegroundColor: Theme disabledForegroundColor: Theme.of(context).colorScheme.onSurface,
.of(context)
.colorScheme
.onSurface,
), ),
onPressed: null, onPressed: null,
icon: Container( icon: Container(
@ -153,17 +153,13 @@ class _FriendsListState extends State<FriendsList> {
margin: const EdgeInsets.only(right: 4), margin: const EdgeInsets.only(right: 4),
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2,
color: Theme color: Theme.of(context).colorScheme.onSurface,
.of(context)
.colorScheme
.onSurface,
), ),
), ),
label: const Text("Loading"), label: const Text("Loading"),
); );
} }
} }),
),
Padding( Padding(
padding: const EdgeInsets.only(left: 4, right: 4), padding: const EdgeInsets.only(left: 4, right: 4),
child: PopupMenuButton<MenuItemDefinition>( child: PopupMenuButton<MenuItemDefinition>(
@ -171,8 +167,7 @@ class _FriendsListState extends State<FriendsList> {
onSelected: (MenuItemDefinition itemDef) async { onSelected: (MenuItemDefinition itemDef) async {
await itemDef.onTap(); await itemDef.onTap();
}, },
itemBuilder: (BuildContext context) => itemBuilder: (BuildContext context) => [
[
MenuItemDefinition( MenuItemDefinition(
name: "Settings", name: "Settings",
icon: Icons.settings, icon: Icons.settings,
@ -187,8 +182,7 @@ class _FriendsListState extends State<FriendsList> {
final mClient = Provider.of<MessagingClient>(context, listen: false); final mClient = Provider.of<MessagingClient>(context, listen: false);
await Navigator.of(context).push( await Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => builder: (context) => ChangeNotifierProvider<MessagingClient>.value(
ChangeNotifierProvider<MessagingClient>.value(
value: mClient, value: mClient,
child: const UserSearch(), child: const UserSearch(),
), ),
@ -208,8 +202,17 @@ class _FriendsListState extends State<FriendsList> {
); );
}, },
), ),
].map((item) => MenuItemDefinition(
PopupMenuItem<MenuItemDefinition>( name: "Sessions",
icon: Icons.location_city,
onTap: () async {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const SessionList()),
);
},
),
].map(
(item) => PopupMenuItem<MenuItemDefinition>(
value: item, value: item,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -219,7 +222,8 @@ class _FriendsListState extends State<FriendsList> {
], ],
), ),
), ),
).toList(), )
.toList(),
), ),
) )
], ],
@ -227,8 +231,7 @@ class _FriendsListState extends State<FriendsList> {
body: Stack( body: Stack(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
children: [ children: [
Consumer<MessagingClient>( Consumer<MessagingClient>(builder: (context, mClient, _) {
builder: (context, mClient, _) {
if (mClient.initStatus == null) { if (mClient.initStatus == null) {
return const LinearProgressIndicator(); return const LinearProgressIndicator();
} else if (mClient.initStatus!.isNotEmpty) { } else if (mClient.initStatus!.isNotEmpty) {
@ -248,8 +251,9 @@ class _FriendsListState extends State<FriendsList> {
} else { } else {
var friends = List.from(mClient.cachedFriends); // Explicit copy. var friends = List.from(mClient.cachedFriends); // Explicit copy.
if (_searchFilter.isNotEmpty) { if (_searchFilter.isNotEmpty) {
friends = friends.where((element) => friends = friends
element.username.toLowerCase().contains(_searchFilter.toLowerCase())).toList(); .where((element) => element.username.toLowerCase().contains(_searchFilter.toLowerCase()))
.toList();
friends.sort((a, b) => a.username.length.compareTo(b.username.length)); friends.sort((a, b) => a.username.length.compareTo(b.username.length));
} }
return ListView.builder( return ListView.builder(
@ -265,8 +269,7 @@ class _FriendsListState extends State<FriendsList> {
}, },
); );
} }
} }),
),
Align( Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: ExpandingInputFab( child: ExpandingInputFab(

View file

@ -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<SessionList> createState() => _SessionListState();
}
class _SessionListState extends State<SessionList> {
Future<List<Session>>? _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<List<Session>>(
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()
],
);
},
),
);
}
}

View file

@ -52,10 +52,13 @@ class SessionView extends StatelessWidget {
), ),
flexibleSpace: FlexibleSpaceBar( flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.pin, collapseMode: CollapseMode.pin,
background: CachedNetworkImage( background: Hero(
tag: session.id,
child: CachedNetworkImage(
imageUrl: Aux.neosDbToHttp(session.thumbnail), imageUrl: Aux.neosDbToHttp(session.thumbnail),
imageBuilder: (context, image) { imageBuilder: (context, image) {
return InkWell( return Material(
child: InkWell(
onTap: () async { onTap: () async {
await Navigator.push( await Navigator.push(
context, context,
@ -68,8 +71,6 @@ class SessionView extends StatelessWidget {
), ),
); );
}, },
child: Hero(
tag: session.id,
child: Image( child: Image(
image: image, image: image,
fit: BoxFit.cover, fit: BoxFit.cover,
@ -85,6 +86,7 @@ class SessionView extends StatelessWidget {
), ),
), ),
), ),
),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 12), padding: const EdgeInsets.only(top: 12),
@ -162,7 +164,7 @@ class SessionView extends StatelessWidget {
SliverList( SliverList(
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) { (BuildContext context, int index) {
final user = session.sessionUsers[index % session.sessionUsers.length]; final user = session.sessionUsers[index];
return ListTile( return ListTile(
dense: true, dense: true,
title: Text( title: Text(
@ -175,7 +177,7 @@ class SessionView extends StatelessWidget {
), ),
); );
}, },
childCount: session.sessionUsers.length * 4, childCount: session.sessionUsers.length,
), ),
) )
], ],