Add filter function to session list

This commit is contained in:
Nutcake 2023-06-04 16:27:18 +02:00
parent 24178aaa43
commit 65207e6e90
16 changed files with 599 additions and 256 deletions

View file

@ -11,8 +11,8 @@ class SessionApi {
return Session.fromMap(body); return Session.fromMap(body);
} }
static Future<List<Session>> getSessions(ApiClient client) async { static Future<List<Session>> getSessions(ApiClient client, {SessionFilterSettings? filterSettings}) async {
final response = await client.get("/sessions"); final response = await client.get("/sessions${filterSettings == null ? "" : filterSettings.buildRequestString()}");
client.checkResponse(response); client.checkResponse(response);
final body = jsonDecode(response.body) as List; final body = jsonDecode(response.body) as List;
return body.map((e) => Session.fromMap(e)).toList(); return body.map((e) => Session.fromMap(e)).toList();

View file

@ -0,0 +1,35 @@
import 'package:collection/collection.dart';
import 'package:contacts_plus_plus/apis/session_api.dart';
import 'package:contacts_plus_plus/clients/api_client.dart';
import 'package:contacts_plus_plus/models/session.dart';
import 'package:flutter/foundation.dart';
class SessionClient extends ChangeNotifier {
final ApiClient apiClient;
Future<List<Session>>? _sessionsFuture;
SessionFilterSettings _filterSettings = SessionFilterSettings.empty();
SessionClient({required this.apiClient}) {
reloadSessions();
}
SessionFilterSettings get filterSettings => _filterSettings;
Future<List<Session>>? get sessionsFuture => _sessionsFuture;
set filterSettings(value) {
_filterSettings = value;
reloadSessions();
}
void reloadSessions() {
_sessionsFuture = SessionApi.getSessions(apiClient, filterSettings: _filterSettings).then(
(value) => value.sorted(
(a, b) => b.sessionUsers.length.compareTo(a.sessionUsers.length),
),
);
notifyListeners();
}
}

View file

@ -4,13 +4,14 @@ import 'package:contacts_plus_plus/apis/github_api.dart';
import 'package:contacts_plus_plus/client_holder.dart'; import 'package:contacts_plus_plus/client_holder.dart';
import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:contacts_plus_plus/clients/api_client.dart';
import 'package:contacts_plus_plus/clients/messaging_client.dart'; import 'package:contacts_plus_plus/clients/messaging_client.dart';
import 'package:contacts_plus_plus/clients/session_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/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/sessions/session_list.dart';
import 'package:contacts_plus_plus/widgets/session_list_app_bar.dart'; import 'package:contacts_plus_plus/widgets/sessions/session_list_app_bar.dart';
import 'package:contacts_plus_plus/widgets/settings_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/settings_page.dart';
import 'package:contacts_plus_plus/widgets/update_notifier.dart'; import 'package:contacts_plus_plus/widgets/update_notifier.dart';
@ -147,21 +148,30 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
darkTheme: ThemeData( darkTheme: ThemeData(
useMaterial3: true, useMaterial3: true,
textTheme: _typography.white, textTheme: _typography.white,
colorScheme: colorScheme: darkDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark),
darkDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark),
), ),
themeMode: ThemeMode.values[widget.settingsClient.currentSettings.themeMode.valueOrDefault], 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 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
? Provider( ? MultiProvider(
providers: [
Provider(
create: (context) => MessagingClient( create: (context) => MessagingClient(
apiClient: clientHolder.apiClient, apiClient: clientHolder.apiClient,
notificationClient: clientHolder.notificationClient, notificationClient: clientHolder.notificationClient,
), ),
dispose: (context, value) => value.dispose(), dispose: (context, value) => value.dispose(),
),
Provider(
create: (context) => SessionClient(
apiClient: clientHolder.apiClient,
),
),
],
child: Scaffold( child: Scaffold(
appBar: PreferredSize( appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight), preferredSize: const Size.fromHeight(kToolbarHeight),
@ -178,7 +188,12 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
SettingsPage(), SettingsPage(),
], ],
), ),
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: Container(
decoration: BoxDecoration(
border: const Border(top: BorderSide(width: 1, color: Colors.black)),
color: Theme.of(context).colorScheme.background,
),
child: BottomNavigationBar(
selectedItemColor: Theme.of(context).colorScheme.primary, selectedItemColor: Theme.of(context).colorScheme.primary,
currentIndex: _selectedPage, currentIndex: _selectedPage,
onTap: (index) { onTap: (index) {
@ -207,6 +222,7 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
], ],
), ),
), ),
),
) )
: LoginScreen( : LoginScreen(
onLoginSuccessful: (AuthenticationData authData) async { onLoginSuccessful: (AuthenticationData authData) async {
@ -217,7 +233,9 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
} }
}, },
); );
})), },
),
),
), ),
); );
}), }),

View file

@ -1,3 +1,5 @@
import 'package:contacts_plus_plus/auxiliary.dart';
import 'package:contacts_plus_plus/config.dart';
import 'package:contacts_plus_plus/string_formatter.dart'; import 'package:contacts_plus_plus/string_formatter.dart';
class Session { class Session {
@ -17,11 +19,22 @@ class Session {
final String hostUsername; final String hostUsername;
final SessionAccessLevel accessLevel; final SessionAccessLevel accessLevel;
Session({required this.id, required this.name, required this.sessionUsers, required this.thumbnail, Session({
required this.maxUsers, required this.hasEnded, required this.isValid, required this.description, required this.id,
required this.tags, required this.headlessHost, required this.hostUserId, required this.hostUsername, required this.name,
required this.sessionUsers,
required this.thumbnail,
required this.maxUsers,
required this.hasEnded,
required this.isValid,
required this.description,
required this.tags,
required this.headlessHost,
required this.hostUserId,
required this.hostUsername,
required this.accessLevel, required this.accessLevel,
}) : formattedName = FormatNode.fromText(name), formattedDescription = FormatNode.fromText(description); }) : formattedName = FormatNode.fromText(name),
formattedDescription = FormatNode.fromText(description);
factory Session.none() { factory Session.none() {
return Session( return Session(
@ -37,8 +50,7 @@ class Session {
headlessHost: false, headlessHost: false,
hostUserId: "", hostUserId: "",
hostUsername: "", hostUsername: "",
accessLevel: SessionAccessLevel.unknown accessLevel: SessionAccessLevel.unknown);
);
} }
bool get isNone => id.isEmpty && isValid == false; bool get isNone => id.isEmpty && isValid == false;
@ -80,8 +92,6 @@ class Session {
}; };
} }
bool get isLive => !hasEnded && isValid; bool get isLive => !hasEnded && isValid;
} }
@ -101,7 +111,8 @@ enum SessionAccessLevel {
}; };
factory SessionAccessLevel.fromName(String? name) { factory SessionAccessLevel.fromName(String? name) {
return SessionAccessLevel.values.firstWhere((element) => element.name.toLowerCase() == name?.toLowerCase(), return SessionAccessLevel.values.firstWhere(
(element) => element.name.toLowerCase() == name?.toLowerCase(),
orElse: () => SessionAccessLevel.unknown, orElse: () => SessionAccessLevel.unknown,
); );
} }
@ -137,3 +148,55 @@ class SessionUser {
}; };
} }
} }
class SessionFilterSettings {
final String name;
final bool includeEnded;
final bool includeIncompatible;
final String hostName;
final int minActiveUsers;
final bool includeEmptyHeadless;
const SessionFilterSettings({
required this.name,
required this.includeEnded,
required this.includeIncompatible,
required this.hostName,
required this.minActiveUsers,
required this.includeEmptyHeadless,
});
factory SessionFilterSettings.empty() => const SessionFilterSettings(
name: "",
includeEnded: false,
includeIncompatible: false,
hostName: "",
minActiveUsers: 0,
includeEmptyHeadless: true,
);
String buildRequestString() => "?includeEmptyHeadless=$includeEmptyHeadless"
"${"&includeEnded=$includeEnded"}"
"${name.isNotEmpty ? "&name=$name" : ""}"
"${!includeIncompatible ? "&compatibilityHash=${Uri.encodeComponent(Config.latestCompatHash)}" : ""}"
"${hostName.isNotEmpty ? "&hostName=$hostName" : ""}"
"${minActiveUsers > 0 ? "&minActiveUsers=$minActiveUsers" : ""}";
SessionFilterSettings copyWith({
String? name,
bool? includeEnded,
bool? includeIncompatible,
String? hostName,
int? minActiveUsers,
bool? includeEmptyHeadless,
}) {
return SessionFilterSettings(
name: name ?? this.name,
includeEnded: includeEnded ?? this.includeEnded,
includeIncompatible: includeIncompatible ?? this.includeIncompatible,
hostName: hostName ?? this.hostName,
minActiveUsers: minActiveUsers ?? this.minActiveUsers,
includeEmptyHeadless: includeEmptyHeadless ?? this.includeEmptyHeadless,
);
}
}

View file

@ -52,6 +52,7 @@ class _FriendsListAppBarState extends State<FriendsListAppBar> with AutomaticKee
value: Provider.of<MessagingClient>(context, listen: false), value: Provider.of<MessagingClient>(context, listen: false),
child: AppBar( child: AppBar(
title: const Text("Contacts++"), title: const Text("Contacts++"),
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
actions: [ actions: [
FutureBuilder( FutureBuilder(
future: _userStatusFuture, future: _userStatusFuture,
@ -206,6 +207,13 @@ class _FriendsListAppBarState extends State<FriendsListAppBar> with AutomaticKee
), ),
) )
], ],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1),
child: Container(
height: 1,
color: Colors.black,
),
),
), ),
); );
} }

View file

@ -1,20 +0,0 @@
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

@ -162,7 +162,6 @@ class _LoginScreenState extends State<LoginScreen> {
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 64), padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 64),
child: TextField( child: TextField(
autofocus: true,
controller: _usernameController, controller: _usernameController,
onEditingComplete: () => _passwordFocusNode.requestFocus(), onEditingComplete: () => _passwordFocusNode.requestFocus(),
decoration: InputDecoration( decoration: InputDecoration(

View file

@ -6,7 +6,7 @@ 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/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/sessions/session_view.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class MessageSessionInvite extends StatelessWidget { class MessageSessionInvite extends StatelessWidget {

View file

@ -3,7 +3,7 @@ import 'package:contacts_plus_plus/auxiliary.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/session_view.dart'; import 'package:contacts_plus_plus/widgets/sessions/session_view.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class SessionPopup extends StatelessWidget { class SessionPopup extends StatelessWidget {

View file

@ -1,147 +0,0 @@
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';
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> with AutomaticKeepAliveClientMixin {
Future<List<Session>>? _sessionsFuture;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_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) {
super.build(context);
return 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()
],
);
},
);
}
@override
bool get wantKeepAlive => true;
}

View file

@ -1,20 +0,0 @@
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,190 @@
import 'dart:math';
import 'package:contacts_plus_plus/clients/session_client.dart';
import 'package:contacts_plus_plus/models/session.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class SessionFilterDialog extends StatefulWidget {
const SessionFilterDialog({required this.lastFilter, super.key});
final SessionFilterSettings lastFilter;
@override
State<StatefulWidget> createState() => _SessionFilterDialogState();
}
class _SessionFilterDialogState extends State<SessionFilterDialog> {
final TextEditingController _sessionNameController = TextEditingController();
final TextEditingController _hostNameController = TextEditingController();
late SessionFilterSettings _currentFilter;
@override
void didUpdateWidget(covariant SessionFilterDialog oldWidget) {
super.didUpdateWidget(oldWidget);
_currentFilter = widget.lastFilter;
if (oldWidget.lastFilter != widget.lastFilter) {
_sessionNameController.text = widget.lastFilter.name;
_hostNameController.text = widget.lastFilter.hostName;
}
}
@override
void initState() {
super.initState();
_currentFilter = widget.lastFilter;
_sessionNameController.text = widget.lastFilter.name;
_hostNameController.text = widget.lastFilter.hostName;
}
@override
void dispose() {
_sessionNameController.dispose();
_hostNameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text("Filter"),
content: SizedBox(
width: double.infinity,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: TextField(
controller: _sessionNameController,
maxLines: 1,
onChanged: (value) {
_currentFilter = _currentFilter.copyWith(name: value);
},
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(32),
),
labelText: 'Session Name',
),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: TextField(
controller: _hostNameController,
onChanged: (value) {
_currentFilter = _currentFilter.copyWith(hostName: value);
},
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(32),
),
labelText: 'Host Name',
),
),
),
Row(
mainAxisSize: MainAxisSize.max,
children: [
const Text("Minimum Users"),
const Spacer(),
IconButton(
onPressed: () {
setState(() {
_currentFilter =
_currentFilter.copyWith(minActiveUsers: max(0, _currentFilter.minActiveUsers - 1));
});
},
icon: const Icon(Icons.remove_circle_outline),
),
Text(
"${_currentFilter.minActiveUsers}",
style: Theme.of(context).textTheme.titleMedium,
),
IconButton(
onPressed: () {
setState(() {
_currentFilter = _currentFilter.copyWith(minActiveUsers: _currentFilter.minActiveUsers + 1, includeEmptyHeadless: false);
});
},
icon: const Icon(Icons.add_circle_outline),
),
],
),
SessionFilterCheckbox(
label: "Include Ended",
value: _currentFilter.includeEnded,
onChanged: (value) {
setState(() {
_currentFilter = _currentFilter.copyWith(includeEnded: value);
});
},
),
SessionFilterCheckbox(
label: "Include Empty Headless",
value: _currentFilter.includeEmptyHeadless && _currentFilter.minActiveUsers == 0,
onChanged: _currentFilter.minActiveUsers > 0 ? null : (value) {
setState(() {
_currentFilter = _currentFilter.copyWith(includeEmptyHeadless: value);
});
},
),
SessionFilterCheckbox(
label: "Include Incompatible",
value: _currentFilter.includeIncompatible,
onChanged: (value) {
setState(() {
_currentFilter = _currentFilter.copyWith(includeIncompatible: value);
});
},
),
],
),
),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text("Cancel"),
),
TextButton(
onPressed: () {
Provider.of<SessionClient>(context, listen: false).filterSettings = _currentFilter;
Navigator.of(context).pop();
},
child: const Text("Okay"),
),
],
);
}
}
class SessionFilterCheckbox extends StatelessWidget {
const SessionFilterCheckbox({required this.label, this.onChanged, this.value, super.key});
final String label;
final void Function(bool? value)? onChanged;
final bool? value;
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label),
Checkbox(
value: value,
onChanged: onChanged,
),
],
);
}
}

View file

@ -0,0 +1,161 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:contacts_plus_plus/auxiliary.dart';
import 'package:contacts_plus_plus/clients/session_client.dart';
import 'package:contacts_plus_plus/models/session.dart';
import 'package:contacts_plus_plus/widgets/default_error_widget.dart';
import 'package:contacts_plus_plus/widgets/formatted_text.dart';
import 'package:contacts_plus_plus/widgets/sessions/session_view.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class SessionList extends StatefulWidget {
const SessionList({super.key});
@override
State<SessionList> createState() => _SessionListState();
}
class _SessionListState extends State<SessionList> with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
return ChangeNotifierProvider.value(
value: Provider.of<SessionClient>(context),
child: Consumer<SessionClient>(
builder: (BuildContext context, SessionClient sClient, Widget? child) {
return FutureBuilder<List<Session>>(
future: sClient.sessionsFuture,
builder: (context, snapshot) {
final data = snapshot.data ?? [];
return Stack(
children: [
RefreshIndicator(
onRefresh: () async {
sClient.reloadSessions();
try {
await sClient.sessionsFuture;
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
}
}
},
child: data.isEmpty && snapshot.connectionState == ConnectionState.done
? const DefaultErrorWidget(
title: "No Sessions Found",
message: "Try to adjust your filters",
iconOverride: Icons.public_off,
)
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: GridView.builder(
padding: const EdgeInsets.only(top: 10),
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()
],
);
},
);
},
),
);
}
@override
bool get wantKeepAlive => true;
}

View file

@ -0,0 +1,48 @@
import 'package:contacts_plus_plus/clients/session_client.dart';
import 'package:contacts_plus_plus/widgets/sessions/session_filter_dialog.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class SessionListAppBar extends StatefulWidget {
const SessionListAppBar({super.key});
@override
State<SessionListAppBar> createState() => _SessionListAppBarState();
}
class _SessionListAppBarState extends State<SessionListAppBar> {
@override
Widget build(BuildContext context) {
return AppBar(
title: const Text("Sessions"),
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1),
child: Container(
height: 1,
color: Colors.black,
),
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 4.0),
child: IconButton(
onPressed: () async {
final sessionClient = Provider.of<SessionClient>(context, listen: false);
await showDialog(
context: context,
builder: (context) => Provider.value(
value: sessionClient,
child: SessionFilterDialog(
lastFilter: sessionClient.filterSettings,
),
),
);
},
icon: const Icon(Icons.filter_alt_outlined),
),
)
],
);
}
}

View file

@ -6,7 +6,15 @@ class SettingsAppBar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppBar( return AppBar(
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
title: const Text("Settings"), title: const Text("Settings"),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1),
child: Container(
height: 1,
color: Colors.black,
),
),
); );
} }