From e87521df9dcaaaf9d9a7cf0e96cc817e0319406e Mon Sep 17 00:00:00 2001 From: Nutcake Date: Tue, 11 Jul 2023 18:44:20 +0200 Subject: [PATCH] Add caching to session filter and remove some redundant providers --- lib/clients/session_client.dart | 23 +- lib/main.dart | 15 +- lib/models/settings.dart | 49 ++- lib/widgets/friends/friends_list.dart | 86 ++-- lib/widgets/friends/friends_list_app_bar.dart | 291 +++++++------- lib/widgets/inventory/inventory_browser.dart | 359 +++++++++-------- .../inventory/inventory_browser_app_bar.dart | 367 +++++++++--------- .../sessions/session_filter_dialog.dart | 31 +- lib/widgets/sessions/session_list.dart | 2 +- .../sessions/session_list_app_bar.dart | 4 +- pubspec.yaml | 2 +- 11 files changed, 639 insertions(+), 590 deletions(-) diff --git a/lib/clients/session_client.dart b/lib/clients/session_client.dart index 6bc4161..7f52ad5 100644 --- a/lib/clients/session_client.dart +++ b/lib/clients/session_client.dart @@ -1,17 +1,28 @@ 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/clients/settings_client.dart'; import 'package:contacts_plus_plus/models/session.dart'; import 'package:flutter/foundation.dart'; class SessionClient extends ChangeNotifier { final ApiClient apiClient; + final SettingsClient settingsClient; Future>? _sessionsFuture; SessionFilterSettings _filterSettings = SessionFilterSettings.empty(); - SessionClient({required this.apiClient}); + SessionClient({required this.apiClient, required this.settingsClient}) { + _filterSettings = SessionFilterSettings( + name: "", + hostName: "", + includeEnded: settingsClient.currentSettings.sessionViewLastIncludeEnded.valueOrDefault, + includeIncompatible: settingsClient.currentSettings.sessionViewLastIncludeIncompatible.valueOrDefault, + minActiveUsers: settingsClient.currentSettings.sessionViewLastMinimumUsers.valueOrDefault, + includeEmptyHeadless: settingsClient.currentSettings.sessionViewLastIncludeEmpty.valueOrDefault, + ); + } SessionFilterSettings get filterSettings => _filterSettings; @@ -22,12 +33,16 @@ class SessionClient extends ChangeNotifier { reloadSessions(); } - void reloadSessions() { + void initSessions() { _sessionsFuture = SessionApi.getSessions(apiClient, filterSettings: _filterSettings).then( - (value) => value.sorted( - (a, b) => b.sessionUsers.length.compareTo(a.sessionUsers.length), + (value) => value.sorted( + (a, b) => b.sessionUsers.length.compareTo(a.sessionUsers.length), ), ); + } + + void reloadSessions() { + initSessions(); notifyListeners(); } } diff --git a/lib/main.dart b/lib/main.dart index ce24809..552f746 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,17 +8,12 @@ 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_app_bar.dart'; import 'package:contacts_plus_plus/widgets/homepage.dart'; -import 'package:contacts_plus_plus/widgets/inventory/inventory_browser_app_bar.dart'; import 'package:contacts_plus_plus/widgets/login_screen.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/update_notifier.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:flutter_phoenix/flutter_phoenix.dart'; import 'package:hive_flutter/hive_flutter.dart'; @@ -35,8 +30,6 @@ void main() async { debug: kDebugMode, ); - Provider.debugCheckInvalidValueType = null; - await Hive.initFlutter(); final dateFormat = DateFormat.Hms(); @@ -156,19 +149,19 @@ class _ContactsPlusPlusState extends State { return _authData.isAuthenticated ? MultiProvider( providers: [ - Provider( + ChangeNotifierProvider( create: (context) => MessagingClient( apiClient: clientHolder.apiClient, notificationClient: clientHolder.notificationClient, ), - dispose: (context, value) => value.dispose(), ), - Provider( + ChangeNotifierProvider( create: (context) => SessionClient( apiClient: clientHolder.apiClient, + settingsClient: clientHolder.settingsClient, ), ), - Provider( + ChangeNotifierProvider( create: (context) => InventoryClient( apiClient: clientHolder.apiClient, ), diff --git a/lib/models/settings.dart b/lib/models/settings.dart index a5410a2..c020b5a 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -40,31 +40,47 @@ class Settings { final SettingsEntry lastDismissedVersion; final SettingsEntry machineId; final SettingsEntry themeMode; + final SettingsEntry sessionViewLastMinimumUsers; + final SettingsEntry sessionViewLastIncludeEnded; + final SettingsEntry sessionViewLastIncludeEmpty; + final SettingsEntry sessionViewLastIncludeIncompatible; Settings({ SettingsEntry? notificationsDenied, SettingsEntry? lastOnlineStatus, SettingsEntry? themeMode, SettingsEntry? lastDismissedVersion, - SettingsEntry? machineId - }) - : notificationsDenied = notificationsDenied ?? const SettingsEntry(deflt: false), + SettingsEntry? machineId, + SettingsEntry? sessionViewLastMinimumUsers, + SettingsEntry? sessionViewLastIncludeEnded, + SettingsEntry? sessionViewLastIncludeEmpty, + SettingsEntry? sessionViewLastIncludeIncompatible, + }) : notificationsDenied = notificationsDenied ?? const SettingsEntry(deflt: false), lastOnlineStatus = lastOnlineStatus ?? SettingsEntry(deflt: OnlineStatus.online.index), themeMode = themeMode ?? SettingsEntry(deflt: ThemeMode.dark.index), lastDismissedVersion = lastDismissedVersion ?? SettingsEntry(deflt: SemVer.zero().toString()), - machineId = machineId ?? SettingsEntry(deflt: const Uuid().v4()); + machineId = machineId ?? SettingsEntry(deflt: const Uuid().v4()), + sessionViewLastMinimumUsers = sessionViewLastMinimumUsers ?? const SettingsEntry(deflt: 0), + sessionViewLastIncludeEnded = sessionViewLastIncludeEnded ?? const SettingsEntry(deflt: false), + sessionViewLastIncludeEmpty = sessionViewLastIncludeEmpty ?? const SettingsEntry(deflt: true), + sessionViewLastIncludeIncompatible = + sessionViewLastIncludeIncompatible ?? const SettingsEntry(deflt: false); factory Settings.fromMap(Map map) { return Settings( - notificationsDenied: retrieveEntryOrNull(map["notificationsDenied"]), - lastOnlineStatus: retrieveEntryOrNull(map["lastOnlineStatus"]), - themeMode: retrieveEntryOrNull(map["themeMode"]), - lastDismissedVersion: retrieveEntryOrNull(map["lastDismissedVersion"]), - machineId: retrieveEntryOrNull(map["machineId"]), + notificationsDenied: getEntryOrNull(map["notificationsDenied"]), + lastOnlineStatus: getEntryOrNull(map["lastOnlineStatus"]), + themeMode: getEntryOrNull(map["themeMode"]), + lastDismissedVersion: getEntryOrNull(map["lastDismissedVersion"]), + machineId: getEntryOrNull(map["machineId"]), + sessionViewLastMinimumUsers: getEntryOrNull(map["sessionViewLastMinimumUsers"]), + sessionViewLastIncludeEnded: getEntryOrNull(map["sessionViewLastIncludeEnded"]), + sessionViewLastIncludeEmpty: getEntryOrNull(map["sessionViewLastIncludeEmpty"]), + sessionViewLastIncludeIncompatible: getEntryOrNull(map["sessionViewLastIncludeIncompatible"]), ); } - static SettingsEntry? retrieveEntryOrNull(Map? map) { + static SettingsEntry? getEntryOrNull(Map? map) { if (map == null) return null; try { return SettingsEntry.fromMap(map); @@ -80,6 +96,10 @@ class Settings { "themeMode": themeMode.toMap(), "lastDismissedVersion": lastDismissedVersion.toMap(), "machineId": machineId.toMap(), + "sessionViewLastMinimumUsers": sessionViewLastMinimumUsers.toMap(), + "sessionViewLastIncludeEnded": sessionViewLastIncludeEnded.toMap(), + "sessionViewLastIncludeEmpty": sessionViewLastIncludeEmpty.toMap(), + "sessionViewLastIncludeIncompatible": sessionViewLastIncludeIncompatible.toMap(), }; } @@ -91,6 +111,10 @@ class Settings { int? themeMode, String? lastDismissedVersion, String? machineId, + int? sessionViewLastMinimumUsers, + bool? sessionViewLastIncludeEnded, + bool? sessionViewLastIncludeEmpty, + bool? sessionViewLastIncludeIncompatible, }) { return Settings( notificationsDenied: this.notificationsDenied.passThrough(notificationsDenied), @@ -98,6 +122,11 @@ class Settings { themeMode: this.themeMode.passThrough(themeMode), lastDismissedVersion: this.lastDismissedVersion.passThrough(lastDismissedVersion), machineId: this.machineId.passThrough(machineId), + sessionViewLastMinimumUsers: this.sessionViewLastMinimumUsers.passThrough(sessionViewLastMinimumUsers), + sessionViewLastIncludeEnded: this.sessionViewLastIncludeEnded.passThrough(sessionViewLastIncludeEnded), + sessionViewLastIncludeEmpty: this.sessionViewLastIncludeEmpty.passThrough(sessionViewLastIncludeEmpty), + sessionViewLastIncludeIncompatible: + this.sessionViewLastIncludeIncompatible.passThrough(sessionViewLastIncludeIncompatible), ); } } \ No newline at end of file diff --git a/lib/widgets/friends/friends_list.dart b/lib/widgets/friends/friends_list.dart index 4739cc7..1cc1637 100644 --- a/lib/widgets/friends/friends_list.dart +++ b/lib/widgets/friends/friends_list.dart @@ -5,7 +5,6 @@ import 'package:contacts_plus_plus/widgets/friends/friend_list_tile.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; - class FriendsList extends StatefulWidget { const FriendsList({super.key}); @@ -19,50 +18,52 @@ class _FriendsListState extends State with AutomaticKeepAliveClient @override Widget build(BuildContext context) { super.build(context); - return ChangeNotifierProvider.value( - value: Provider.of(context, listen: false), - child: Stack( + return Consumer( + builder: (context, mClient, _) { + return Stack( alignment: Alignment.topCenter, children: [ - Consumer(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(); - }, + Builder( + builder: (context) { + 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)); + ], + ); + } 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, + ); + }, + ); } - 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( @@ -81,7 +82,8 @@ class _FriendsListState extends State with AutomaticKeepAliveClient ), ), ], - ), + ); + }, ); } diff --git a/lib/widgets/friends/friends_list_app_bar.dart b/lib/widgets/friends/friends_list_app_bar.dart index 7e5ef23..e76c922 100644 --- a/lib/widgets/friends/friends_list_app_bar.dart +++ b/lib/widgets/friends/friends_list_app_bar.dart @@ -47,158 +47,155 @@ class _FriendsListAppBarState extends State with AutomaticKee @override Widget build(BuildContext context) { super.build(context); - return ChangeNotifierProvider.value( - value: Provider.of(context, listen: false), - child: AppBar( - title: const Text("Contacts++"), - actions: [ - FutureBuilder( - future: _userStatusFuture, - builder: (context, snapshot) { - if (snapshot.hasData) { - final userStatus = snapshot.data as UserStatus; - return PopupMenuButton( - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Icon( - Icons.circle, - size: 16, - color: userStatus.onlineStatus.color(context), - ), - ), - Text(toBeginningOfSentenceCase(userStatus.onlineStatus.name) ?? "Unknown"), - ], - ), - onSelected: (OnlineStatus onlineStatus) async { - try { - final newStatus = userStatus.copyWith(onlineStatus: onlineStatus); - setState(() { - _userStatusFuture = Future.value(newStatus.copyWith(lastStatusChange: DateTime.now())); - }); - final settingsClient = _clientHolder!.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."))); - setState(() { - _userStatusFuture = Future.value(userStatus); - }); - } - }, - itemBuilder: (BuildContext context) => OnlineStatus.values - .where((element) => element == OnlineStatus.online || element == OnlineStatus.invisible) - .map( - (item) => PopupMenuItem( - 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()); - } else if (snapshot.hasError) { - return TextButton.icon( - style: TextButton.styleFrom( - 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); - }); - }, - icon: const Icon(Icons.warning), - label: const Text("Retry"), - ); - } else { - return TextButton.icon( - style: TextButton.styleFrom( - disabledForegroundColor: Theme.of(context).colorScheme.onSurface, - ), - onPressed: null, - icon: Container( - width: 16, - height: 16, - margin: const EdgeInsets.only(right: 4), - child: CircularProgressIndicator( - strokeWidth: 2, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - label: const Text("Loading"), - ); - } - }, - ), - Padding( - padding: const EdgeInsets.only(left: 4, right: 4), - child: PopupMenuButton( - icon: const Icon(Icons.more_vert), - onSelected: (MenuItemDefinition itemDef) async { - await itemDef.onTap(); - }, - itemBuilder: (BuildContext context) => [ - MenuItemDefinition( - name: "Find Users", - icon: Icons.person_add, - onTap: () async { - final mClient = Provider.of(context, listen: false); - await Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => ChangeNotifierProvider.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( - value: item, + return AppBar( + title: const Text("Contacts++"), + actions: [ + FutureBuilder( + future: _userStatusFuture, + builder: (context, snapshot) { + if (snapshot.hasData) { + final userStatus = snapshot.data as UserStatus; + return PopupMenuButton( child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(item.name), - Icon(item.icon), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Icon( + Icons.circle, + size: 16, + color: userStatus.onlineStatus.color(context), + ), + ), + Text(toBeginningOfSentenceCase(userStatus.onlineStatus.name) ?? "Unknown"), ], ), + onSelected: (OnlineStatus onlineStatus) async { + try { + final newStatus = userStatus.copyWith(onlineStatus: onlineStatus); + setState(() { + _userStatusFuture = Future.value(newStatus.copyWith(lastStatusChange: DateTime.now())); + }); + final settingsClient = _clientHolder!.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."))); + setState(() { + _userStatusFuture = Future.value(userStatus); + }); + } + }, + itemBuilder: (BuildContext context) => OnlineStatus.values + .where((element) => element == OnlineStatus.online || element == OnlineStatus.invisible) + .map( + (item) => PopupMenuItem( + 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()); + } else if (snapshot.hasError) { + return TextButton.icon( + style: TextButton.styleFrom( + 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); + }); + }, + icon: const Icon(Icons.warning), + label: const Text("Retry"), + ); + } else { + return TextButton.icon( + style: TextButton.styleFrom( + disabledForegroundColor: Theme.of(context).colorScheme.onSurface, ), - ) - .toList(), - ), - ) - ], - ), + onPressed: null, + icon: Container( + width: 16, + height: 16, + margin: const EdgeInsets.only(right: 4), + child: CircularProgressIndicator( + strokeWidth: 2, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + label: const Text("Loading"), + ); + } + }, + ), + Padding( + padding: const EdgeInsets.only(left: 4, right: 4), + child: PopupMenuButton( + icon: const Icon(Icons.more_vert), + onSelected: (MenuItemDefinition itemDef) async { + await itemDef.onTap(); + }, + itemBuilder: (BuildContext context) => [ + MenuItemDefinition( + name: "Find Users", + icon: Icons.person_add, + onTap: () async { + final mClient = Provider.of(context, listen: false); + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ChangeNotifierProvider.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( + value: item, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(item.name), + Icon(item.icon), + ], + ), + ), + ) + .toList(), + ), + ) + ], ); } diff --git a/lib/widgets/inventory/inventory_browser.dart b/lib/widgets/inventory/inventory_browser.dart index bf34f43..b58d831 100644 --- a/lib/widgets/inventory/inventory_browser.dart +++ b/lib/widgets/inventory/inventory_browser.dart @@ -36,196 +36,193 @@ class _InventoryBrowserState extends State with AutomaticKeepA @override Widget build(BuildContext context) { super.build(context); - return ChangeNotifierProvider.value( - value: Provider.of(context), - child: Consumer(builder: (BuildContext context, InventoryClient iClient, Widget? child) { - return FutureBuilder( - future: iClient.directoryFuture, - builder: (context, snapshot) { - final currentDir = snapshot.data; - return WillPopScope( - onWillPop: () async { - // Allow pop when at root or not loaded - if (currentDir?.isRoot ?? true) { - return true; + return Consumer(builder: (BuildContext context, InventoryClient iClient, Widget? child) { + return FutureBuilder( + future: iClient.directoryFuture, + builder: (context, snapshot) { + final currentDir = snapshot.data; + return WillPopScope( + onWillPop: () async { + // Allow pop when at root or not loaded + if (currentDir?.isRoot ?? true) { + return true; + } + iClient.navigateUp(); + return false; + }, + child: RefreshIndicator( + onRefresh: () async { + if (_refreshLimiter?.isActive ?? false) return; + try { + await iClient.reloadCurrentDirectory(); + _refreshLimiter = Timer(_refreshLimit, () {}); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Refresh failed: $e"))); } - iClient.navigateUp(); - return false; }, - child: RefreshIndicator( - onRefresh: () async { - if (_refreshLimiter?.isActive ?? false) return; - try { - await iClient.reloadCurrentDirectory(); - _refreshLimiter = Timer(_refreshLimit, () {}); - } catch (e) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Refresh failed: $e"))); + child: Builder( + builder: (context) { + if (snapshot.hasError) { + FlutterError.reportError( + FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace)); + return DefaultErrorWidget( + message: snapshot.error.toString(), + onRetry: () { + iClient.loadInventoryRoot(); + iClient.forceNotify(); + }, + ); } - }, - child: Builder( - builder: (context) { - if (snapshot.hasError) { - FlutterError.reportError( - FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace)); - return DefaultErrorWidget( - message: snapshot.error.toString(), - onRetry: () { - iClient.loadInventoryRoot(); - iClient.forceNotify(); - }, - ); - } - final directory = snapshot.data; - final records = directory?.records ?? []; + final directory = snapshot.data; + final records = directory?.records ?? []; - records.sort((a, b) => a.name.compareTo(b.name)); - final paths = records - .where((element) => - element.recordType == RecordType.link || element.recordType == RecordType.directory) - .toList(); - final objects = records - .where((element) => - element.recordType != RecordType.link && element.recordType != RecordType.directory) - .toList(); - final pathSegments = directory?.absolutePathSegments ?? []; - return Stack( - children: [ - ListView( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), - child: Wrap( - children: pathSegments - .mapIndexed( - (idx, segment) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (idx != 0) const Icon(Icons.chevron_right), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: TextButton( - style: TextButton.styleFrom( - foregroundColor: idx == pathSegments.length - 1 - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface, - ), - onPressed: () { - iClient.navigateUp(times: pathSegments.length - 1 - idx); - }, - child: Text(segment), + records.sort((a, b) => a.name.compareTo(b.name)); + final paths = records + .where((element) => + element.recordType == RecordType.link || element.recordType == RecordType.directory) + .toList(); + final objects = records + .where((element) => + element.recordType != RecordType.link && element.recordType != RecordType.directory) + .toList(); + final pathSegments = directory?.absolutePathSegments ?? []; + return Stack( + children: [ + ListView( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), + child: Wrap( + children: pathSegments + .mapIndexed( + (idx, segment) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (idx != 0) const Icon(Icons.chevron_right), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: TextButton( + style: TextButton.styleFrom( + foregroundColor: idx == pathSegments.length - 1 + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurface, + ), + onPressed: () { + iClient.navigateUp(times: pathSegments.length - 1 - idx); + }, + child: Text(segment), + ), + ), + ], + ), + ) + .toList(), + ), + ), + GridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: paths.length, + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 256, + childAspectRatio: 3.5, + crossAxisSpacing: 0, + mainAxisSpacing: 0), + itemBuilder: (context, index) { + final record = paths[index]; + return PathInventoryTile( + record: record, + onTap: iClient.isAnyRecordSelected + ? () {} + : () async { + try { + await iClient.navigateTo(record); + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Failed to open directory: $e")), + ); + } + } + }, + ); + }, + ), + const SizedBox( + height: 0, + ), + GridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: objects.length, + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 256, + childAspectRatio: 1, + crossAxisSpacing: 0, + mainAxisSpacing: 0, + ), + itemBuilder: (context, index) { + final record = objects[index]; + return ObjectInventoryTile( + record: record, + selected: iClient.isRecordSelected(record), + onTap: iClient.isAnyRecordSelected + ? () async { + iClient.toggleRecordSelected(record); + } + : () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PhotoView( + minScale: PhotoViewComputedScale.contained, + imageProvider: + CachedNetworkImageProvider(Aux.neosDbToHttp(record.thumbnailUri)), + heroAttributes: PhotoViewHeroAttributes(tag: record.id), ), ), - ], - ), - ) - .toList(), - ), - ), - GridView.builder( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: paths.length, - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 256, - childAspectRatio: 3.5, - crossAxisSpacing: 0, - mainAxisSpacing: 0), - itemBuilder: (context, index) { - final record = paths[index]; - return PathInventoryTile( - record: record, - onTap: iClient.isAnyRecordSelected - ? () {} - : () async { - try { - await iClient.navigateTo(record); - } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Failed to open directory: $e")), - ); - } - } - }, - ); - }, - ), - const SizedBox( - height: 0, - ), - GridView.builder( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: objects.length, - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 256, - childAspectRatio: 1, - crossAxisSpacing: 0, - mainAxisSpacing: 0, - ), - itemBuilder: (context, index) { - final record = objects[index]; - return ObjectInventoryTile( - record: record, - selected: iClient.isRecordSelected(record), - onTap: iClient.isAnyRecordSelected - ? () async { - iClient.toggleRecordSelected(record); - } - : () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PhotoView( - minScale: PhotoViewComputedScale.contained, - imageProvider: - CachedNetworkImageProvider(Aux.neosDbToHttp(record.thumbnailUri)), - heroAttributes: PhotoViewHeroAttributes(tag: record.id), - ), - ), - ); - }, - onLongPress: () async { - iClient.toggleRecordSelected(record); - }, - ); - }, - ), - ], - ), - Align( - alignment: Alignment.topCenter, - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - child: snapshot.connectionState == ConnectionState.waiting - ? const LinearProgressIndicator() - : null, + ); + }, + onLongPress: () async { + iClient.toggleRecordSelected(record); + }, + ); + }, ), + ], + ), + Align( + alignment: Alignment.topCenter, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: snapshot.connectionState == ConnectionState.waiting + ? const LinearProgressIndicator() + : null, ), - Align( - alignment: Alignment.topCenter, - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - child: snapshot.connectionState == ConnectionState.waiting - ? Container( - width: double.infinity, - height: double.infinity, - color: Colors.black38, - ) - : null, - ), - ) - ], - ); - }, - ), + ), + Align( + alignment: Alignment.topCenter, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: snapshot.connectionState == ConnectionState.waiting + ? Container( + width: double.infinity, + height: double.infinity, + color: Colors.black38, + ) + : null, + ), + ) + ], + ); + }, ), - ); - }); - }), - ); + ), + ); + }); + }); } @override diff --git a/lib/widgets/inventory/inventory_browser_app_bar.dart b/lib/widgets/inventory/inventory_browser_app_bar.dart index 5722847..9721ebd 100644 --- a/lib/widgets/inventory/inventory_browser_app_bar.dart +++ b/lib/widgets/inventory/inventory_browser_app_bar.dart @@ -48,202 +48,199 @@ class _InventoryBrowserAppBarState extends State { @override Widget build(BuildContext context) { - return ChangeNotifierProvider.value( - value: Provider.of(context), - child: Consumer( - builder: (BuildContext context, InventoryClient iClient, Widget? child) { - return AnimatedSwitcher( - duration: const Duration(milliseconds: 350), - transitionBuilder: (child, animation) => FadeTransition( - opacity: animation, - child: child, - ), - child: !iClient.isAnyRecordSelected - ? AppBar( - key: const ValueKey("default-appbar"), - title: const Text("Inventory"), - ) - : AppBar( - key: const ValueKey("selection-appbar"), - title: Text("${iClient.selectedRecordCount} Selected"), - leading: IconButton( - onPressed: () { - iClient.clearSelectedRecords(); - }, - icon: const Icon(Icons.close), - ), - actions: [ - if (iClient.onlyFilesSelected) - IconButton( - onPressed: () async { - final selectedRecords = iClient.selectedRecords; - - final assetUris = selectedRecords.map((record) => record.assetUri).toList(); - final thumbUris = selectedRecords.map((record) => record.thumbnailUri).toList(); - - final selectedUris = await showDialog>( - context: context, - builder: (context) { - return AlertDialog( - icon: const Icon(Icons.download), - title: const Text("Download what?"), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - const Divider(), - const SizedBox( - height: 8, - ), - TextButton.icon( - onPressed: () { - Navigator.of(context).pop(assetUris); - }, - icon: const Icon(Icons.data_object), - label: Text( - "Asset${iClient.selectedRecordCount != 1 ? "s" : ""} (${assetUris.map((e) => extension(e)).toList().unique().join(", ")})", - ), - ), - TextButton.icon( - onPressed: () { - Navigator.of(context).pop(thumbUris); - }, - icon: const Icon(Icons.image), - label: Text( - "Thumbnail${iClient.selectedRecordCount != 1 ? "s" : ""} (${thumbUris.map((e) => extension(e)).toList().unique().join(", ")})", - ), - ), - ], - ), - ); - }, - ); - if (selectedUris == null) return; - - final directory = await FilePicker.platform.getDirectoryPath(dialogTitle: "Download to..."); - if (directory == null) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Selection aborted."), - ), - ); - } - return; - } - if (directory == "/") { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Selected directory is invalid"), - ), - ); - } - return; - } - for (var record in selectedRecords) { - final uri = selectedUris == thumbUris ? record.thumbnailUri : record.thumbnailUri; - await FlutterDownloader.enqueue( - url: Aux.neosDbToHttp(uri), - savedDir: directory, - showNotification: true, - openFileFromNotification: false, - fileName: - "${record.id.split("-")[1]}-${record.formattedName.toString()}${extension(uri)}", - ); - } - iClient.clearSelectedRecords(); - }, - icon: const Icon(Icons.download), - ), - const SizedBox( - width: 4, - ), + return Consumer( + builder: (BuildContext context, InventoryClient iClient, Widget? child) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 350), + transitionBuilder: (child, animation) => FadeTransition( + opacity: animation, + child: child, + ), + child: !iClient.isAnyRecordSelected + ? AppBar( + key: const ValueKey("default-appbar"), + title: const Text("Inventory"), + ) + : AppBar( + key: const ValueKey("selection-appbar"), + title: Text("${iClient.selectedRecordCount} Selected"), + leading: IconButton( + onPressed: () { + iClient.clearSelectedRecords(); + }, + icon: const Icon(Icons.close), + ), + actions: [ + if (iClient.onlyFilesSelected) IconButton( onPressed: () async { - var loading = false; - await showDialog( + final selectedRecords = iClient.selectedRecords; + + final assetUris = selectedRecords.map((record) => record.assetUri).toList(); + final thumbUris = selectedRecords.map((record) => record.thumbnailUri).toList(); + + final selectedUris = await showDialog>( context: context, builder: (context) { - return StatefulBuilder( - builder: (context, setState) { - return AlertDialog( - icon: const Icon(Icons.delete), - title: Text(iClient.selectedRecordCount == 1 - ? "Really delete this Record?" - : "Really delete ${iClient.selectedRecordCount} Records?"), - content: const Text("This action cannot be undone!"), - actionsAlignment: MainAxisAlignment.spaceBetween, - actions: [ - TextButton( - onPressed: loading - ? null - : () { - Navigator.of(context).pop(false); - }, - child: const Text("Cancel"), + return AlertDialog( + icon: const Icon(Icons.download), + title: const Text("Download what?"), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Divider(), + const SizedBox( + height: 8, + ), + TextButton.icon( + onPressed: () { + Navigator.of(context).pop(assetUris); + }, + icon: const Icon(Icons.data_object), + label: Text( + "Asset${iClient.selectedRecordCount != 1 ? "s" : ""} (${assetUris.map((e) => extension(e)).toList().unique().join(", ")})", ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (loading) - const SizedBox.square( - dimension: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ), - const SizedBox( - width: 4, - ), - TextButton( - onPressed: loading - ? null - : () async { - setState(() { - loading = true; - }); - try { - await iClient.deleteSelectedRecords(); - } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Failed to delete one or more records: $e"), - ), - ); - } - setState(() { - loading = false; - }); - } - if (context.mounted) { - Navigator.of(context).pop(true); - } - iClient.reloadCurrentDirectory(); - }, - style: TextButton.styleFrom( - foregroundColor: Theme.of(context).colorScheme.error, - ), - child: const Text("Delete"), - ), - ], + ), + TextButton.icon( + onPressed: () { + Navigator.of(context).pop(thumbUris); + }, + icon: const Icon(Icons.image), + label: Text( + "Thumbnail${iClient.selectedRecordCount != 1 ? "s" : ""} (${thumbUris.map((e) => extension(e)).toList().unique().join(", ")})", ), - ], - ); - }, + ), + ], + ), ); }, ); + if (selectedUris == null) return; + + final directory = await FilePicker.platform.getDirectoryPath(dialogTitle: "Download to..."); + if (directory == null) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Selection aborted."), + ), + ); + } + return; + } + if (directory == "/") { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Selected directory is invalid"), + ), + ); + } + return; + } + for (var record in selectedRecords) { + final uri = selectedUris == thumbUris ? record.thumbnailUri : record.thumbnailUri; + await FlutterDownloader.enqueue( + url: Aux.neosDbToHttp(uri), + savedDir: directory, + showNotification: true, + openFileFromNotification: false, + fileName: + "${record.id.split("-")[1]}-${record.formattedName.toString()}${extension(uri)}", + ); + } + iClient.clearSelectedRecords(); }, - icon: const Icon(Icons.delete), + icon: const Icon(Icons.download), ), - const SizedBox( - width: 4, - ), - ], - ), - ); - }, - ), + const SizedBox( + width: 4, + ), + IconButton( + onPressed: () async { + var loading = false; + await showDialog( + context: context, + builder: (context) { + return StatefulBuilder( + builder: (context, setState) { + return AlertDialog( + icon: const Icon(Icons.delete), + title: Text(iClient.selectedRecordCount == 1 + ? "Really delete this Record?" + : "Really delete ${iClient.selectedRecordCount} Records?"), + content: const Text("This action cannot be undone!"), + actionsAlignment: MainAxisAlignment.spaceBetween, + actions: [ + TextButton( + onPressed: loading + ? null + : () { + Navigator.of(context).pop(false); + }, + child: const Text("Cancel"), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (loading) + const SizedBox.square( + dimension: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ), + const SizedBox( + width: 4, + ), + TextButton( + onPressed: loading + ? null + : () async { + setState(() { + loading = true; + }); + try { + await iClient.deleteSelectedRecords(); + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Failed to delete one or more records: $e"), + ), + ); + } + setState(() { + loading = false; + }); + } + if (context.mounted) { + Navigator.of(context).pop(true); + } + iClient.reloadCurrentDirectory(); + }, + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.error, + ), + child: const Text("Delete"), + ), + ], + ), + ], + ); + }, + ); + }, + ); + }, + icon: const Icon(Icons.delete), + ), + const SizedBox( + width: 4, + ), + ], + ), + ); + }, ); } } diff --git a/lib/widgets/sessions/session_filter_dialog.dart b/lib/widgets/sessions/session_filter_dialog.dart index 891373e..926e44a 100644 --- a/lib/widgets/sessions/session_filter_dialog.dart +++ b/lib/widgets/sessions/session_filter_dialog.dart @@ -1,6 +1,8 @@ import 'dart:math'; +import 'package:contacts_plus_plus/client_holder.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/session.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -44,9 +46,20 @@ class _SessionFilterDialogState extends State { super.dispose(); } + Future _updateSettings() async { + final settingsClient = ClientHolder.of(context).settingsClient; + await settingsClient.changeSettings(settingsClient.currentSettings.copyWith( + sessionViewLastMinimumUsers: _currentFilter.minActiveUsers, + sessionViewLastIncludeEnded: _currentFilter.includeEnded, + sessionViewLastIncludeEmpty: _currentFilter.includeEmptyHeadless, + sessionViewLastIncludeIncompatible: _currentFilter.includeIncompatible, + )); + } + @override Widget build(BuildContext context) { return AlertDialog( + insetPadding: const EdgeInsets.all(24), title: const Text("Filter"), content: SizedBox( width: double.infinity, @@ -109,7 +122,8 @@ class _SessionFilterDialogState extends State { IconButton( onPressed: () { setState(() { - _currentFilter = _currentFilter.copyWith(minActiveUsers: _currentFilter.minActiveUsers + 1, includeEmptyHeadless: false); + _currentFilter = _currentFilter.copyWith( + minActiveUsers: _currentFilter.minActiveUsers + 1, includeEmptyHeadless: false); }); }, icon: const Icon(Icons.add_circle_outline), @@ -128,11 +142,13 @@ class _SessionFilterDialogState extends State { SessionFilterCheckbox( label: "Include Empty Headless", value: _currentFilter.includeEmptyHeadless && _currentFilter.minActiveUsers == 0, - onChanged: _currentFilter.minActiveUsers > 0 ? null : (value) { - setState(() { - _currentFilter = _currentFilter.copyWith(includeEmptyHeadless: value); - }); - }, + onChanged: _currentFilter.minActiveUsers > 0 + ? null + : (value) { + setState(() { + _currentFilter = _currentFilter.copyWith(includeEmptyHeadless: value); + }); + }, ), SessionFilterCheckbox( label: "Include Incompatible", @@ -155,9 +171,10 @@ class _SessionFilterDialogState extends State { child: const Text("Cancel"), ), TextButton( - onPressed: () { + onPressed: () async { Provider.of(context, listen: false).filterSettings = _currentFilter; Navigator.of(context).pop(); + await _updateSettings(); }, child: const Text("Okay"), ), diff --git a/lib/widgets/sessions/session_list.dart b/lib/widgets/sessions/session_list.dart index 0f8ee1f..6d4b3c6 100644 --- a/lib/widgets/sessions/session_list.dart +++ b/lib/widgets/sessions/session_list.dart @@ -21,7 +21,7 @@ class _SessionListState extends State with AutomaticKeepAliveClient super.didChangeDependencies(); final sClient = Provider.of(context, listen: false); if (sClient.sessionsFuture == null) { - sClient.reloadSessions(); + sClient.initSessions(); } } diff --git a/lib/widgets/sessions/session_list_app_bar.dart b/lib/widgets/sessions/session_list_app_bar.dart index aa19421..0743988 100644 --- a/lib/widgets/sessions/session_list_app_bar.dart +++ b/lib/widgets/sessions/session_list_app_bar.dart @@ -1,4 +1,6 @@ +import 'package:contacts_plus_plus/client_holder.dart'; import 'package:contacts_plus_plus/clients/session_client.dart'; +import 'package:contacts_plus_plus/clients/settings_client.dart'; import 'package:contacts_plus_plus/widgets/sessions/session_filter_dialog.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -23,7 +25,7 @@ class _SessionListAppBarState extends State { final sessionClient = Provider.of(context, listen: false); await showDialog( context: context, - builder: (context) => Provider.value( + builder: (context) => ChangeNotifierProvider.value( value: sessionClient, child: SessionFilterDialog( lastFilter: sessionClient.filterSettings, diff --git a/pubspec.yaml b/pubspec.yaml index 69d5548..f105c7f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.4.1+1 +version: 1.4.2+1 environment: sdk: '>=3.0.1'