Add caching to session filter and remove some redundant providers
This commit is contained in:
parent
56350ea2c7
commit
e87521df9d
11 changed files with 639 additions and 590 deletions
|
@ -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<List<Session>>? _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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ContactsPlusPlus> {
|
|||
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,
|
||||
),
|
||||
|
|
|
@ -40,31 +40,47 @@ class Settings {
|
|||
final SettingsEntry<String> lastDismissedVersion;
|
||||
final SettingsEntry<String> machineId;
|
||||
final SettingsEntry<int> themeMode;
|
||||
final SettingsEntry<int> sessionViewLastMinimumUsers;
|
||||
final SettingsEntry<bool> sessionViewLastIncludeEnded;
|
||||
final SettingsEntry<bool> sessionViewLastIncludeEmpty;
|
||||
final SettingsEntry<bool> sessionViewLastIncludeIncompatible;
|
||||
|
||||
Settings({
|
||||
SettingsEntry<bool>? notificationsDenied,
|
||||
SettingsEntry<int>? lastOnlineStatus,
|
||||
SettingsEntry<int>? themeMode,
|
||||
SettingsEntry<String>? lastDismissedVersion,
|
||||
SettingsEntry<String>? machineId
|
||||
})
|
||||
: notificationsDenied = notificationsDenied ?? const SettingsEntry<bool>(deflt: false),
|
||||
SettingsEntry<String>? machineId,
|
||||
SettingsEntry<int>? sessionViewLastMinimumUsers,
|
||||
SettingsEntry<bool>? sessionViewLastIncludeEnded,
|
||||
SettingsEntry<bool>? sessionViewLastIncludeEmpty,
|
||||
SettingsEntry<bool>? sessionViewLastIncludeIncompatible,
|
||||
}) : notificationsDenied = notificationsDenied ?? const SettingsEntry<bool>(deflt: false),
|
||||
lastOnlineStatus = lastOnlineStatus ?? SettingsEntry<int>(deflt: OnlineStatus.online.index),
|
||||
themeMode = themeMode ?? SettingsEntry<int>(deflt: ThemeMode.dark.index),
|
||||
lastDismissedVersion = lastDismissedVersion ?? SettingsEntry<String>(deflt: SemVer.zero().toString()),
|
||||
machineId = machineId ?? SettingsEntry<String>(deflt: const Uuid().v4());
|
||||
machineId = machineId ?? SettingsEntry<String>(deflt: const Uuid().v4()),
|
||||
sessionViewLastMinimumUsers = sessionViewLastMinimumUsers ?? const SettingsEntry<int>(deflt: 0),
|
||||
sessionViewLastIncludeEnded = sessionViewLastIncludeEnded ?? const SettingsEntry<bool>(deflt: false),
|
||||
sessionViewLastIncludeEmpty = sessionViewLastIncludeEmpty ?? const SettingsEntry<bool>(deflt: true),
|
||||
sessionViewLastIncludeIncompatible =
|
||||
sessionViewLastIncludeIncompatible ?? const SettingsEntry<bool>(deflt: false);
|
||||
|
||||
factory Settings.fromMap(Map map) {
|
||||
return Settings(
|
||||
notificationsDenied: retrieveEntryOrNull<bool>(map["notificationsDenied"]),
|
||||
lastOnlineStatus: retrieveEntryOrNull<int>(map["lastOnlineStatus"]),
|
||||
themeMode: retrieveEntryOrNull<int>(map["themeMode"]),
|
||||
lastDismissedVersion: retrieveEntryOrNull<String>(map["lastDismissedVersion"]),
|
||||
machineId: retrieveEntryOrNull<String>(map["machineId"]),
|
||||
notificationsDenied: getEntryOrNull<bool>(map["notificationsDenied"]),
|
||||
lastOnlineStatus: getEntryOrNull<int>(map["lastOnlineStatus"]),
|
||||
themeMode: getEntryOrNull<int>(map["themeMode"]),
|
||||
lastDismissedVersion: getEntryOrNull<String>(map["lastDismissedVersion"]),
|
||||
machineId: getEntryOrNull<String>(map["machineId"]),
|
||||
sessionViewLastMinimumUsers: getEntryOrNull<int>(map["sessionViewLastMinimumUsers"]),
|
||||
sessionViewLastIncludeEnded: getEntryOrNull<bool>(map["sessionViewLastIncludeEnded"]),
|
||||
sessionViewLastIncludeEmpty: getEntryOrNull<bool>(map["sessionViewLastIncludeEmpty"]),
|
||||
sessionViewLastIncludeIncompatible: getEntryOrNull<bool>(map["sessionViewLastIncludeIncompatible"]),
|
||||
);
|
||||
}
|
||||
|
||||
static SettingsEntry<T>? retrieveEntryOrNull<T>(Map? map) {
|
||||
static SettingsEntry<T>? getEntryOrNull<T>(Map? map) {
|
||||
if (map == null) return null;
|
||||
try {
|
||||
return SettingsEntry<T>.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),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<FriendsList> with AutomaticKeepAliveClient
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return ChangeNotifierProvider.value(
|
||||
value: Provider.of<MessagingClient>(context, listen: false),
|
||||
child: Stack(
|
||||
return Consumer<MessagingClient>(
|
||||
builder: (context, mClient, _) {
|
||||
return Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
Consumer<MessagingClient>(builder: (context, mClient, _) {
|
||||
if (mClient.initStatus == null) {
|
||||
return const LinearProgressIndicator();
|
||||
} else if (mClient.initStatus!.isNotEmpty) {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: DefaultErrorWidget(
|
||||
message: mClient.initStatus,
|
||||
onRetry: () async {
|
||||
mClient.resetInitStatus();
|
||||
mClient.refreshFriendsListWithErrorHandler();
|
||||
},
|
||||
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<FriendsList> with AutomaticKeepAliveClient
|
|||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -47,158 +47,155 @@ class _FriendsListAppBarState extends State<FriendsListAppBar> with AutomaticKee
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return ChangeNotifierProvider.value(
|
||||
value: Provider.of<MessagingClient>(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<OnlineStatus>(
|
||||
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<OnlineStatus>(
|
||||
value: item,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.circle,
|
||||
size: 16,
|
||||
color: item.color(context),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Text(toBeginningOfSentenceCase(item.name)!),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList());
|
||||
} 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<MenuItemDefinition>(
|
||||
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<MessagingClient>(context, listen: false);
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChangeNotifierProvider<MessagingClient>.value(
|
||||
value: mClient,
|
||||
child: const UserSearch(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuItemDefinition(
|
||||
name: "My Profile",
|
||||
icon: Icons.person,
|
||||
onTap: () async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return const MyProfileDialog();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
]
|
||||
.map(
|
||||
(item) => PopupMenuItem<MenuItemDefinition>(
|
||||
value: item,
|
||||
return AppBar(
|
||||
title: const Text("Contacts++"),
|
||||
actions: [
|
||||
FutureBuilder(
|
||||
future: _userStatusFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final userStatus = snapshot.data as UserStatus;
|
||||
return PopupMenuButton<OnlineStatus>(
|
||||
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<OnlineStatus>(
|
||||
value: item,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.circle,
|
||||
size: 16,
|
||||
color: item.color(context),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Text(toBeginningOfSentenceCase(item.name)!),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList());
|
||||
} 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<MenuItemDefinition>(
|
||||
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<MessagingClient>(context, listen: false);
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChangeNotifierProvider<MessagingClient>.value(
|
||||
value: mClient,
|
||||
child: const UserSearch(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuItemDefinition(
|
||||
name: "My Profile",
|
||||
icon: Icons.person,
|
||||
onTap: () async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return const MyProfileDialog();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
]
|
||||
.map(
|
||||
(item) => PopupMenuItem<MenuItemDefinition>(
|
||||
value: item,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(item.name),
|
||||
Icon(item.icon),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -36,196 +36,193 @@ class _InventoryBrowserState extends State<InventoryBrowser> with AutomaticKeepA
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return ChangeNotifierProvider.value(
|
||||
value: Provider.of<InventoryClient>(context),
|
||||
child: Consumer<InventoryClient>(builder: (BuildContext context, InventoryClient iClient, Widget? child) {
|
||||
return FutureBuilder<NeosDirectory>(
|
||||
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<InventoryClient>(builder: (BuildContext context, InventoryClient iClient, Widget? child) {
|
||||
return FutureBuilder<NeosDirectory>(
|
||||
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
|
||||
|
|
|
@ -48,202 +48,199 @@ class _InventoryBrowserAppBarState extends State<InventoryBrowserAppBar> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: Provider.of<InventoryClient>(context),
|
||||
child: Consumer<InventoryClient>(
|
||||
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<List<String>>(
|
||||
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<InventoryClient>(
|
||||
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<List<String>>(
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<SessionFilterDialog> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _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<SessionFilterDialog> {
|
|||
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<SessionFilterDialog> {
|
|||
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<SessionFilterDialog> {
|
|||
child: const Text("Cancel"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
Provider.of<SessionClient>(context, listen: false).filterSettings = _currentFilter;
|
||||
Navigator.of(context).pop();
|
||||
await _updateSettings();
|
||||
},
|
||||
child: const Text("Okay"),
|
||||
),
|
||||
|
|
|
@ -21,7 +21,7 @@ class _SessionListState extends State<SessionList> with AutomaticKeepAliveClient
|
|||
super.didChangeDependencies();
|
||||
final sClient = Provider.of<SessionClient>(context, listen: false);
|
||||
if (sClient.sessionsFuture == null) {
|
||||
sClient.reloadSessions();
|
||||
sClient.initSessions();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<SessionListAppBar> {
|
|||
final sessionClient = Provider.of<SessionClient>(context, listen: false);
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => Provider.value(
|
||||
builder: (context) => ChangeNotifierProvider.value(
|
||||
value: sessionClient,
|
||||
child: SessionFilterDialog(
|
||||
lastFilter: sessionClient.filterSettings,
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in a new issue