Add filter function to session list
This commit is contained in:
parent
24178aaa43
commit
65207e6e90
16 changed files with 599 additions and 256 deletions
|
@ -11,8 +11,8 @@ class SessionApi {
|
|||
return Session.fromMap(body);
|
||||
}
|
||||
|
||||
static Future<List<Session>> getSessions(ApiClient client) async {
|
||||
final response = await client.get("/sessions");
|
||||
static Future<List<Session>> getSessions(ApiClient client, {SessionFilterSettings? filterSettings}) async {
|
||||
final response = await client.get("/sessions${filterSettings == null ? "" : filterSettings.buildRequestString()}");
|
||||
client.checkResponse(response);
|
||||
final body = jsonDecode(response.body) as List;
|
||||
return body.map((e) => Session.fromMap(e)).toList();
|
||||
|
|
35
lib/clients/session_client.dart
Normal file
35
lib/clients/session_client.dart
Normal 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();
|
||||
}
|
||||
}
|
124
lib/main.dart
124
lib/main.dart
|
@ -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/clients/api_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/models/sem_ver.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/session_list.dart';
|
||||
import 'package:contacts_plus_plus/widgets/session_list_app_bar.dart';
|
||||
import 'package:contacts_plus_plus/widgets/sessions/session_list.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_page.dart';
|
||||
import 'package:contacts_plus_plus/widgets/update_notifier.dart';
|
||||
|
@ -136,32 +137,41 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
|
|||
},
|
||||
child: DynamicColorBuilder(
|
||||
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) => MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'Contacts++',
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
textTheme: _typography.black,
|
||||
colorScheme:
|
||||
lightDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.light),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
textTheme: _typography.white,
|
||||
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
|
||||
builder: (context) {
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'Contacts++',
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
textTheme: _typography.black,
|
||||
colorScheme:
|
||||
lightDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.light),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
textTheme: _typography.white,
|
||||
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
|
||||
builder: (context) {
|
||||
showUpdateDialogOnFirstBuild(context);
|
||||
final clientHolder = ClientHolder.of(context);
|
||||
return _authData.isAuthenticated
|
||||
? Provider(
|
||||
create: (context) => MessagingClient(
|
||||
apiClient: clientHolder.apiClient,
|
||||
notificationClient: clientHolder.notificationClient,
|
||||
),
|
||||
dispose: (context, value) => value.dispose(),
|
||||
? MultiProvider(
|
||||
providers: [
|
||||
Provider(
|
||||
create: (context) => MessagingClient(
|
||||
apiClient: clientHolder.apiClient,
|
||||
notificationClient: clientHolder.notificationClient,
|
||||
),
|
||||
dispose: (context, value) => value.dispose(),
|
||||
),
|
||||
Provider(
|
||||
create: (context) => SessionClient(
|
||||
apiClient: clientHolder.apiClient,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||
|
@ -178,33 +188,39 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
|
|||
SettingsPage(),
|
||||
],
|
||||
),
|
||||
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",
|
||||
),
|
||||
],
|
||||
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,
|
||||
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",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -217,7 +233,9 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
|
|||
}
|
||||
},
|
||||
);
|
||||
})),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
|
|
@ -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';
|
||||
|
||||
class Session {
|
||||
|
@ -17,11 +19,22 @@ class Session {
|
|||
final String hostUsername;
|
||||
final SessionAccessLevel accessLevel;
|
||||
|
||||
Session({required this.id, 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,
|
||||
Session({
|
||||
required this.id,
|
||||
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,
|
||||
}) : formattedName = FormatNode.fromText(name), formattedDescription = FormatNode.fromText(description);
|
||||
}) : formattedName = FormatNode.fromText(name),
|
||||
formattedDescription = FormatNode.fromText(description);
|
||||
|
||||
factory Session.none() {
|
||||
return Session(
|
||||
|
@ -37,8 +50,7 @@ class Session {
|
|||
headlessHost: false,
|
||||
hostUserId: "",
|
||||
hostUsername: "",
|
||||
accessLevel: SessionAccessLevel.unknown
|
||||
);
|
||||
accessLevel: SessionAccessLevel.unknown);
|
||||
}
|
||||
|
||||
bool get isNone => id.isEmpty && isValid == false;
|
||||
|
@ -62,7 +74,7 @@ class Session {
|
|||
);
|
||||
}
|
||||
|
||||
Map toMap({bool shallow=false}) {
|
||||
Map toMap({bool shallow = false}) {
|
||||
return {
|
||||
"sessionId": id,
|
||||
"name": name,
|
||||
|
@ -80,8 +92,6 @@ class Session {
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
bool get isLive => !hasEnded && isValid;
|
||||
}
|
||||
|
||||
|
@ -101,7 +111,8 @@ enum SessionAccessLevel {
|
|||
};
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
@ -136,4 +147,56 @@ class SessionUser {
|
|||
"outputDevice": outputDevice,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ class _FriendsListAppBarState extends State<FriendsListAppBar> with AutomaticKee
|
|||
value: Provider.of<MessagingClient>(context, listen: false),
|
||||
child: AppBar(
|
||||
title: const Text("Contacts++"),
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||
actions: [
|
||||
FutureBuilder(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
||||
);
|
||||
}
|
||||
}
|
|
@ -162,7 +162,6 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 64),
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
controller: _usernameController,
|
||||
onEditingComplete: () => _passwordFocusNode.requestFocus(),
|
||||
decoration: InputDecoration(
|
||||
|
|
|
@ -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/generic_avatar.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';
|
||||
|
||||
class MessageSessionInvite extends StatelessWidget {
|
||||
|
|
|
@ -3,7 +3,7 @@ import 'package:contacts_plus_plus/auxiliary.dart';
|
|||
import 'package:contacts_plus_plus/models/session.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/session_view.dart';
|
||||
import 'package:contacts_plus_plus/widgets/sessions/session_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SessionPopup extends StatelessWidget {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
190
lib/widgets/sessions/session_filter_dialog.dart
Normal file
190
lib/widgets/sessions/session_filter_dialog.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
161
lib/widgets/sessions/session_list.dart
Normal file
161
lib/widgets/sessions/session_list.dart
Normal 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;
|
||||
}
|
48
lib/widgets/sessions/session_list_app_bar.dart
Normal file
48
lib/widgets/sessions/session_list_app_bar.dart
Normal 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),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -6,7 +6,15 @@ class SettingsAppBar extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||
title: const Text("Settings"),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(1),
|
||||
child: Container(
|
||||
height: 1,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue