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);
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/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<FriendsList> {
_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<FriendsList> {
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<FriendsList> {
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<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)!),
],
),
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());
),
)
.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<FriendsList> {
} 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<FriendsList> {
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<MenuItemDefinition>(
@ -171,55 +167,63 @@ class _FriendsListState extends State<FriendsList> {
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),
],
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(),
),
),
).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<MenuItemDefinition>(
value: item,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(item.name),
Icon(item.icon),
],
),
),
)
.toList(),
),
)
],
@ -227,46 +231,45 @@ class _FriendsListState extends State<FriendsList> {
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,
);
},
);
}
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(
@ -288,4 +291,4 @@ class _FriendsListState extends State<FriendsList> {
),
);
}
}
}

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,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,
),
)
],