diff --git a/lib/apis/session_api.dart b/lib/apis/session_api.dart index 952e45d..a453fbd 100644 --- a/lib/apis/session_api.dart +++ b/lib/apis/session_api.dart @@ -11,8 +11,8 @@ class SessionApi { return Session.fromMap(body); } - static Future> getSessions(ApiClient client) async { - final response = await client.get("/sessions"); + static Future> 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(); diff --git a/lib/clients/session_client.dart b/lib/clients/session_client.dart new file mode 100644 index 0000000..37ff518 --- /dev/null +++ b/lib/clients/session_client.dart @@ -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>? _sessionsFuture; + + SessionFilterSettings _filterSettings = SessionFilterSettings.empty(); + + SessionClient({required this.apiClient}) { + reloadSessions(); + } + + SessionFilterSettings get filterSettings => _filterSettings; + + Future>? 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(); + } +} diff --git a/lib/main.dart b/lib/main.dart index 8f3a027..dee27e1 100644 --- a/lib/main.dart +++ b/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 { }, 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 { 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 { } }, ); - })), + }, + ), + ), ), ); }), diff --git a/lib/models/session.dart b/lib/models/session.dart index a2c428d..03aa1c3 100644 --- a/lib/models/session.dart +++ b/lib/models/session.dart @@ -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, }; } -} \ No newline at end of file +} + +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, + ); + } +} diff --git a/lib/widgets/friends/friends_list_app_bar.dart b/lib/widgets/friends/friends_list_app_bar.dart index 020eff8..07b2ab1 100644 --- a/lib/widgets/friends/friends_list_app_bar.dart +++ b/lib/widgets/friends/friends_list_app_bar.dart @@ -52,6 +52,7 @@ class _FriendsListAppBarState extends State with AutomaticKee value: Provider.of(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 with AutomaticKee ), ) ], + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1), + child: Container( + height: 1, + color: Colors.black, + ), + ), ), ); } diff --git a/lib/widgets/global_app_bar.dart b/lib/widgets/global_app_bar.dart deleted file mode 100644 index 74dbb15..0000000 --- a/lib/widgets/global_app_bar.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; - -class GlobalAppBar extends StatefulWidget implements PreferredSizeWidget { - const GlobalAppBar({super.key}); - - @override - State createState() => _GlobalAppBarState(); - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} - -class _GlobalAppBarState extends State { - @override - Widget build(BuildContext context) { - return AppBar( - - ); - } -} \ No newline at end of file diff --git a/lib/widgets/login_screen.dart b/lib/widgets/login_screen.dart index 541caf9..ac147d6 100644 --- a/lib/widgets/login_screen.dart +++ b/lib/widgets/login_screen.dart @@ -162,7 +162,6 @@ class _LoginScreenState extends State { Padding( padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 64), child: TextField( - autofocus: true, controller: _usernameController, onEditingComplete: () => _passwordFocusNode.requestFocus(), decoration: InputDecoration( diff --git a/lib/widgets/messages/message_session_invite.dart b/lib/widgets/messages/message_session_invite.dart index ee52cb2..6754bb7 100644 --- a/lib/widgets/messages/message_session_invite.dart +++ b/lib/widgets/messages/message_session_invite.dart @@ -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 { diff --git a/lib/widgets/messages/messages_session_header.dart b/lib/widgets/messages/messages_session_header.dart index 13a9be1..34439ca 100644 --- a/lib/widgets/messages/messages_session_header.dart +++ b/lib/widgets/messages/messages_session_header.dart @@ -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 { diff --git a/lib/widgets/session_list.dart b/lib/widgets/session_list.dart deleted file mode 100644 index 157301b..0000000 --- a/lib/widgets/session_list.dart +++ /dev/null @@ -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 createState() => _SessionListState(); -} - -class _SessionListState extends State with AutomaticKeepAliveClientMixin { - Future>? _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>( - 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; -} diff --git a/lib/widgets/session_list_app_bar.dart b/lib/widgets/session_list_app_bar.dart deleted file mode 100644 index 3175049..0000000 --- a/lib/widgets/session_list_app_bar.dart +++ /dev/null @@ -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), - ) - ], - ); - } -} diff --git a/lib/widgets/sessions/session_filter_dialog.dart b/lib/widgets/sessions/session_filter_dialog.dart new file mode 100644 index 0000000..891373e --- /dev/null +++ b/lib/widgets/sessions/session_filter_dialog.dart @@ -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 createState() => _SessionFilterDialogState(); +} + +class _SessionFilterDialogState extends State { + 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(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, + ), + ], + ); + } +} diff --git a/lib/widgets/sessions/session_list.dart b/lib/widgets/sessions/session_list.dart new file mode 100644 index 0000000..a221e2f --- /dev/null +++ b/lib/widgets/sessions/session_list.dart @@ -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 createState() => _SessionListState(); +} + +class _SessionListState extends State with AutomaticKeepAliveClientMixin { + @override + Widget build(BuildContext context) { + super.build(context); + return ChangeNotifierProvider.value( + value: Provider.of(context), + child: Consumer( + builder: (BuildContext context, SessionClient sClient, Widget? child) { + return FutureBuilder>( + 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; +} diff --git a/lib/widgets/sessions/session_list_app_bar.dart b/lib/widgets/sessions/session_list_app_bar.dart new file mode 100644 index 0000000..f3efbd3 --- /dev/null +++ b/lib/widgets/sessions/session_list_app_bar.dart @@ -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 createState() => _SessionListAppBarState(); +} + +class _SessionListAppBarState extends State { + @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(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), + ), + ) + ], + ); + } +} diff --git a/lib/widgets/session_view.dart b/lib/widgets/sessions/session_view.dart similarity index 100% rename from lib/widgets/session_view.dart rename to lib/widgets/sessions/session_view.dart diff --git a/lib/widgets/settings_app_bar.dart b/lib/widgets/settings_app_bar.dart index 5433939..1b226bf 100644 --- a/lib/widgets/settings_app_bar.dart +++ b/lib/widgets/settings_app_bar.dart @@ -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, + ), + ), ); }