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:collection/collection.dart';
|
||||||
import 'package:contacts_plus_plus/apis/session_api.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/api_client.dart';
|
||||||
|
import 'package:contacts_plus_plus/clients/settings_client.dart';
|
||||||
import 'package:contacts_plus_plus/models/session.dart';
|
import 'package:contacts_plus_plus/models/session.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
class SessionClient extends ChangeNotifier {
|
class SessionClient extends ChangeNotifier {
|
||||||
final ApiClient apiClient;
|
final ApiClient apiClient;
|
||||||
|
final SettingsClient settingsClient;
|
||||||
|
|
||||||
Future<List<Session>>? _sessionsFuture;
|
Future<List<Session>>? _sessionsFuture;
|
||||||
|
|
||||||
SessionFilterSettings _filterSettings = SessionFilterSettings.empty();
|
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;
|
SessionFilterSettings get filterSettings => _filterSettings;
|
||||||
|
|
||||||
|
@ -22,12 +33,16 @@ class SessionClient extends ChangeNotifier {
|
||||||
reloadSessions();
|
reloadSessions();
|
||||||
}
|
}
|
||||||
|
|
||||||
void reloadSessions() {
|
void initSessions() {
|
||||||
_sessionsFuture = SessionApi.getSessions(apiClient, filterSettings: _filterSettings).then(
|
_sessionsFuture = SessionApi.getSessions(apiClient, filterSettings: _filterSettings).then(
|
||||||
(value) => value.sorted(
|
(value) => value.sorted(
|
||||||
(a, b) => b.sessionUsers.length.compareTo(a.sessionUsers.length),
|
(a, b) => b.sessionUsers.length.compareTo(a.sessionUsers.length),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void reloadSessions() {
|
||||||
|
initSessions();
|
||||||
notifyListeners();
|
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/session_client.dart';
|
||||||
import 'package:contacts_plus_plus/clients/settings_client.dart';
|
import 'package:contacts_plus_plus/clients/settings_client.dart';
|
||||||
import 'package:contacts_plus_plus/models/sem_ver.dart';
|
import 'package:contacts_plus_plus/models/sem_ver.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/friends/friends_list_app_bar.dart';
|
|
||||||
import 'package:contacts_plus_plus/widgets/homepage.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/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:contacts_plus_plus/widgets/update_notifier.dart';
|
||||||
import 'package:dynamic_color/dynamic_color.dart';
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_downloader/flutter_downloader.dart';
|
import 'package:flutter_downloader/flutter_downloader.dart';
|
||||||
import 'package:flutter_phoenix/flutter_phoenix.dart';
|
import 'package:flutter_phoenix/flutter_phoenix.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
|
@ -35,8 +30,6 @@ void main() async {
|
||||||
debug: kDebugMode,
|
debug: kDebugMode,
|
||||||
);
|
);
|
||||||
|
|
||||||
Provider.debugCheckInvalidValueType = null;
|
|
||||||
|
|
||||||
await Hive.initFlutter();
|
await Hive.initFlutter();
|
||||||
|
|
||||||
final dateFormat = DateFormat.Hms();
|
final dateFormat = DateFormat.Hms();
|
||||||
|
@ -156,19 +149,19 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
|
||||||
return _authData.isAuthenticated
|
return _authData.isAuthenticated
|
||||||
? MultiProvider(
|
? MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
Provider(
|
ChangeNotifierProvider(
|
||||||
create: (context) => MessagingClient(
|
create: (context) => MessagingClient(
|
||||||
apiClient: clientHolder.apiClient,
|
apiClient: clientHolder.apiClient,
|
||||||
notificationClient: clientHolder.notificationClient,
|
notificationClient: clientHolder.notificationClient,
|
||||||
),
|
),
|
||||||
dispose: (context, value) => value.dispose(),
|
|
||||||
),
|
),
|
||||||
Provider(
|
ChangeNotifierProvider(
|
||||||
create: (context) => SessionClient(
|
create: (context) => SessionClient(
|
||||||
apiClient: clientHolder.apiClient,
|
apiClient: clientHolder.apiClient,
|
||||||
|
settingsClient: clientHolder.settingsClient,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Provider(
|
ChangeNotifierProvider(
|
||||||
create: (context) => InventoryClient(
|
create: (context) => InventoryClient(
|
||||||
apiClient: clientHolder.apiClient,
|
apiClient: clientHolder.apiClient,
|
||||||
),
|
),
|
||||||
|
|
|
@ -40,31 +40,47 @@ class Settings {
|
||||||
final SettingsEntry<String> lastDismissedVersion;
|
final SettingsEntry<String> lastDismissedVersion;
|
||||||
final SettingsEntry<String> machineId;
|
final SettingsEntry<String> machineId;
|
||||||
final SettingsEntry<int> themeMode;
|
final SettingsEntry<int> themeMode;
|
||||||
|
final SettingsEntry<int> sessionViewLastMinimumUsers;
|
||||||
|
final SettingsEntry<bool> sessionViewLastIncludeEnded;
|
||||||
|
final SettingsEntry<bool> sessionViewLastIncludeEmpty;
|
||||||
|
final SettingsEntry<bool> sessionViewLastIncludeIncompatible;
|
||||||
|
|
||||||
Settings({
|
Settings({
|
||||||
SettingsEntry<bool>? notificationsDenied,
|
SettingsEntry<bool>? notificationsDenied,
|
||||||
SettingsEntry<int>? lastOnlineStatus,
|
SettingsEntry<int>? lastOnlineStatus,
|
||||||
SettingsEntry<int>? themeMode,
|
SettingsEntry<int>? themeMode,
|
||||||
SettingsEntry<String>? lastDismissedVersion,
|
SettingsEntry<String>? lastDismissedVersion,
|
||||||
SettingsEntry<String>? machineId
|
SettingsEntry<String>? machineId,
|
||||||
})
|
SettingsEntry<int>? sessionViewLastMinimumUsers,
|
||||||
: notificationsDenied = notificationsDenied ?? const SettingsEntry<bool>(deflt: false),
|
SettingsEntry<bool>? sessionViewLastIncludeEnded,
|
||||||
|
SettingsEntry<bool>? sessionViewLastIncludeEmpty,
|
||||||
|
SettingsEntry<bool>? sessionViewLastIncludeIncompatible,
|
||||||
|
}) : notificationsDenied = notificationsDenied ?? const SettingsEntry<bool>(deflt: false),
|
||||||
lastOnlineStatus = lastOnlineStatus ?? SettingsEntry<int>(deflt: OnlineStatus.online.index),
|
lastOnlineStatus = lastOnlineStatus ?? SettingsEntry<int>(deflt: OnlineStatus.online.index),
|
||||||
themeMode = themeMode ?? SettingsEntry<int>(deflt: ThemeMode.dark.index),
|
themeMode = themeMode ?? SettingsEntry<int>(deflt: ThemeMode.dark.index),
|
||||||
lastDismissedVersion = lastDismissedVersion ?? SettingsEntry<String>(deflt: SemVer.zero().toString()),
|
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) {
|
factory Settings.fromMap(Map map) {
|
||||||
return Settings(
|
return Settings(
|
||||||
notificationsDenied: retrieveEntryOrNull<bool>(map["notificationsDenied"]),
|
notificationsDenied: getEntryOrNull<bool>(map["notificationsDenied"]),
|
||||||
lastOnlineStatus: retrieveEntryOrNull<int>(map["lastOnlineStatus"]),
|
lastOnlineStatus: getEntryOrNull<int>(map["lastOnlineStatus"]),
|
||||||
themeMode: retrieveEntryOrNull<int>(map["themeMode"]),
|
themeMode: getEntryOrNull<int>(map["themeMode"]),
|
||||||
lastDismissedVersion: retrieveEntryOrNull<String>(map["lastDismissedVersion"]),
|
lastDismissedVersion: getEntryOrNull<String>(map["lastDismissedVersion"]),
|
||||||
machineId: retrieveEntryOrNull<String>(map["machineId"]),
|
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;
|
if (map == null) return null;
|
||||||
try {
|
try {
|
||||||
return SettingsEntry<T>.fromMap(map);
|
return SettingsEntry<T>.fromMap(map);
|
||||||
|
@ -80,6 +96,10 @@ class Settings {
|
||||||
"themeMode": themeMode.toMap(),
|
"themeMode": themeMode.toMap(),
|
||||||
"lastDismissedVersion": lastDismissedVersion.toMap(),
|
"lastDismissedVersion": lastDismissedVersion.toMap(),
|
||||||
"machineId": machineId.toMap(),
|
"machineId": machineId.toMap(),
|
||||||
|
"sessionViewLastMinimumUsers": sessionViewLastMinimumUsers.toMap(),
|
||||||
|
"sessionViewLastIncludeEnded": sessionViewLastIncludeEnded.toMap(),
|
||||||
|
"sessionViewLastIncludeEmpty": sessionViewLastIncludeEmpty.toMap(),
|
||||||
|
"sessionViewLastIncludeIncompatible": sessionViewLastIncludeIncompatible.toMap(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,6 +111,10 @@ class Settings {
|
||||||
int? themeMode,
|
int? themeMode,
|
||||||
String? lastDismissedVersion,
|
String? lastDismissedVersion,
|
||||||
String? machineId,
|
String? machineId,
|
||||||
|
int? sessionViewLastMinimumUsers,
|
||||||
|
bool? sessionViewLastIncludeEnded,
|
||||||
|
bool? sessionViewLastIncludeEmpty,
|
||||||
|
bool? sessionViewLastIncludeIncompatible,
|
||||||
}) {
|
}) {
|
||||||
return Settings(
|
return Settings(
|
||||||
notificationsDenied: this.notificationsDenied.passThrough(notificationsDenied),
|
notificationsDenied: this.notificationsDenied.passThrough(notificationsDenied),
|
||||||
|
@ -98,6 +122,11 @@ class Settings {
|
||||||
themeMode: this.themeMode.passThrough(themeMode),
|
themeMode: this.themeMode.passThrough(themeMode),
|
||||||
lastDismissedVersion: this.lastDismissedVersion.passThrough(lastDismissedVersion),
|
lastDismissedVersion: this.lastDismissedVersion.passThrough(lastDismissedVersion),
|
||||||
machineId: this.machineId.passThrough(machineId),
|
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:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
|
||||||
class FriendsList extends StatefulWidget {
|
class FriendsList extends StatefulWidget {
|
||||||
const FriendsList({super.key});
|
const FriendsList({super.key});
|
||||||
|
|
||||||
|
@ -19,50 +18,52 @@ class _FriendsListState extends State<FriendsList> with AutomaticKeepAliveClient
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
return ChangeNotifierProvider.value(
|
return Consumer<MessagingClient>(
|
||||||
value: Provider.of<MessagingClient>(context, listen: false),
|
builder: (context, mClient, _) {
|
||||||
child: Stack(
|
return Stack(
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
children: [
|
children: [
|
||||||
Consumer<MessagingClient>(builder: (context, mClient, _) {
|
Builder(
|
||||||
if (mClient.initStatus == null) {
|
builder: (context) {
|
||||||
return const LinearProgressIndicator();
|
if (mClient.initStatus == null) {
|
||||||
} else if (mClient.initStatus!.isNotEmpty) {
|
return const LinearProgressIndicator();
|
||||||
return Column(
|
} else if (mClient.initStatus!.isNotEmpty) {
|
||||||
children: [
|
return Column(
|
||||||
Expanded(
|
children: [
|
||||||
child: DefaultErrorWidget(
|
Expanded(
|
||||||
message: mClient.initStatus,
|
child: DefaultErrorWidget(
|
||||||
onRetry: () async {
|
message: mClient.initStatus,
|
||||||
mClient.resetInitStatus();
|
onRetry: () async {
|
||||||
mClient.refreshFriendsListWithErrorHandler();
|
mClient.resetInitStatus();
|
||||||
},
|
mClient.refreshFriendsListWithErrorHandler();
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
);
|
||||||
);
|
} else {
|
||||||
} else {
|
var friends = List.from(mClient.cachedFriends); // Explicit copy.
|
||||||
var friends = List.from(mClient.cachedFriends); // Explicit copy.
|
if (_searchFilter.isNotEmpty) {
|
||||||
if (_searchFilter.isNotEmpty) {
|
friends = friends
|
||||||
friends = friends
|
.where((element) => element.username.toLowerCase().contains(_searchFilter.toLowerCase()))
|
||||||
.where((element) => element.username.toLowerCase().contains(_searchFilter.toLowerCase()))
|
.toList();
|
||||||
.toList();
|
friends.sort((a, b) => a.username.length.compareTo(b.username.length));
|
||||||
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(
|
Align(
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
child: ExpandingInputFab(
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
return ChangeNotifierProvider.value(
|
return AppBar(
|
||||||
value: Provider.of<MessagingClient>(context, listen: false),
|
title: const Text("Contacts++"),
|
||||||
child: AppBar(
|
actions: [
|
||||||
title: const Text("Contacts++"),
|
FutureBuilder(
|
||||||
actions: [
|
future: _userStatusFuture,
|
||||||
FutureBuilder(
|
builder: (context, snapshot) {
|
||||||
future: _userStatusFuture,
|
if (snapshot.hasData) {
|
||||||
builder: (context, snapshot) {
|
final userStatus = snapshot.data as UserStatus;
|
||||||
if (snapshot.hasData) {
|
return PopupMenuButton<OnlineStatus>(
|
||||||
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,
|
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
Text(item.name),
|
Padding(
|
||||||
Icon(item.icon),
|
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,
|
||||||
.toList(),
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
return ChangeNotifierProvider.value(
|
return Consumer<InventoryClient>(builder: (BuildContext context, InventoryClient iClient, Widget? child) {
|
||||||
value: Provider.of<InventoryClient>(context),
|
return FutureBuilder<NeosDirectory>(
|
||||||
child: Consumer<InventoryClient>(builder: (BuildContext context, InventoryClient iClient, Widget? child) {
|
future: iClient.directoryFuture,
|
||||||
return FutureBuilder<NeosDirectory>(
|
builder: (context, snapshot) {
|
||||||
future: iClient.directoryFuture,
|
final currentDir = snapshot.data;
|
||||||
builder: (context, snapshot) {
|
return WillPopScope(
|
||||||
final currentDir = snapshot.data;
|
onWillPop: () async {
|
||||||
return WillPopScope(
|
// Allow pop when at root or not loaded
|
||||||
onWillPop: () async {
|
if (currentDir?.isRoot ?? true) {
|
||||||
// Allow pop when at root or not loaded
|
return true;
|
||||||
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(
|
child: Builder(
|
||||||
onRefresh: () async {
|
builder: (context) {
|
||||||
if (_refreshLimiter?.isActive ?? false) return;
|
if (snapshot.hasError) {
|
||||||
try {
|
FlutterError.reportError(
|
||||||
await iClient.reloadCurrentDirectory();
|
FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace));
|
||||||
_refreshLimiter = Timer(_refreshLimit, () {});
|
return DefaultErrorWidget(
|
||||||
} catch (e) {
|
message: snapshot.error.toString(),
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Refresh failed: $e")));
|
onRetry: () {
|
||||||
|
iClient.loadInventoryRoot();
|
||||||
|
iClient.forceNotify();
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
final directory = snapshot.data;
|
||||||
child: Builder(
|
final records = directory?.records ?? [];
|
||||||
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 ?? [];
|
|
||||||
|
|
||||||
records.sort((a, b) => a.name.compareTo(b.name));
|
records.sort((a, b) => a.name.compareTo(b.name));
|
||||||
final paths = records
|
final paths = records
|
||||||
.where((element) =>
|
.where((element) =>
|
||||||
element.recordType == RecordType.link || element.recordType == RecordType.directory)
|
element.recordType == RecordType.link || element.recordType == RecordType.directory)
|
||||||
.toList();
|
.toList();
|
||||||
final objects = records
|
final objects = records
|
||||||
.where((element) =>
|
.where((element) =>
|
||||||
element.recordType != RecordType.link && element.recordType != RecordType.directory)
|
element.recordType != RecordType.link && element.recordType != RecordType.directory)
|
||||||
.toList();
|
.toList();
|
||||||
final pathSegments = directory?.absolutePathSegments ?? [];
|
final pathSegments = directory?.absolutePathSegments ?? [];
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
ListView(
|
ListView(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
children: pathSegments
|
children: pathSegments
|
||||||
.mapIndexed(
|
.mapIndexed(
|
||||||
(idx, segment) => Row(
|
(idx, segment) => Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (idx != 0) const Icon(Icons.chevron_right),
|
if (idx != 0) const Icon(Icons.chevron_right),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: idx == pathSegments.length - 1
|
foregroundColor: idx == pathSegments.length - 1
|
||||||
? Theme.of(context).colorScheme.primary
|
? Theme.of(context).colorScheme.primary
|
||||||
: Theme.of(context).colorScheme.onSurface,
|
: Theme.of(context).colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
iClient.navigateUp(times: pathSegments.length - 1 - idx);
|
iClient.navigateUp(times: pathSegments.length - 1 - idx);
|
||||||
},
|
},
|
||||||
child: Text(segment),
|
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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
),
|
},
|
||||||
)
|
onLongPress: () async {
|
||||||
.toList(),
|
iClient.toggleRecordSelected(record);
|
||||||
),
|
},
|
||||||
),
|
);
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
child: snapshot.connectionState == ConnectionState.waiting
|
||||||
|
? const LinearProgressIndicator()
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
Align(
|
),
|
||||||
alignment: Alignment.topCenter,
|
Align(
|
||||||
child: AnimatedSwitcher(
|
alignment: Alignment.topCenter,
|
||||||
duration: const Duration(milliseconds: 250),
|
child: AnimatedSwitcher(
|
||||||
child: snapshot.connectionState == ConnectionState.waiting
|
duration: const Duration(milliseconds: 250),
|
||||||
? Container(
|
child: snapshot.connectionState == ConnectionState.waiting
|
||||||
width: double.infinity,
|
? Container(
|
||||||
height: double.infinity,
|
width: double.infinity,
|
||||||
color: Colors.black38,
|
height: double.infinity,
|
||||||
)
|
color: Colors.black38,
|
||||||
: null,
|
)
|
||||||
),
|
: null,
|
||||||
)
|
),
|
||||||
],
|
)
|
||||||
);
|
],
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
});
|
);
|
||||||
}),
|
});
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -48,202 +48,199 @@ class _InventoryBrowserAppBarState extends State<InventoryBrowserAppBar> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProvider.value(
|
return Consumer<InventoryClient>(
|
||||||
value: Provider.of<InventoryClient>(context),
|
builder: (BuildContext context, InventoryClient iClient, Widget? child) {
|
||||||
child: Consumer<InventoryClient>(
|
return AnimatedSwitcher(
|
||||||
builder: (BuildContext context, InventoryClient iClient, Widget? child) {
|
duration: const Duration(milliseconds: 350),
|
||||||
return AnimatedSwitcher(
|
transitionBuilder: (child, animation) => FadeTransition(
|
||||||
duration: const Duration(milliseconds: 350),
|
opacity: animation,
|
||||||
transitionBuilder: (child, animation) => FadeTransition(
|
child: child,
|
||||||
opacity: animation,
|
),
|
||||||
child: child,
|
child: !iClient.isAnyRecordSelected
|
||||||
),
|
? AppBar(
|
||||||
child: !iClient.isAnyRecordSelected
|
key: const ValueKey("default-appbar"),
|
||||||
? AppBar(
|
title: const Text("Inventory"),
|
||||||
key: const ValueKey("default-appbar"),
|
)
|
||||||
title: const Text("Inventory"),
|
: AppBar(
|
||||||
)
|
key: const ValueKey("selection-appbar"),
|
||||||
: AppBar(
|
title: Text("${iClient.selectedRecordCount} Selected"),
|
||||||
key: const ValueKey("selection-appbar"),
|
leading: IconButton(
|
||||||
title: Text("${iClient.selectedRecordCount} Selected"),
|
onPressed: () {
|
||||||
leading: IconButton(
|
iClient.clearSelectedRecords();
|
||||||
onPressed: () {
|
},
|
||||||
iClient.clearSelectedRecords();
|
icon: const Icon(Icons.close),
|
||||||
},
|
),
|
||||||
icon: const Icon(Icons.close),
|
actions: [
|
||||||
),
|
if (iClient.onlyFilesSelected)
|
||||||
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,
|
|
||||||
),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
var loading = false;
|
final selectedRecords = iClient.selectedRecords;
|
||||||
await showDialog(
|
|
||||||
|
final assetUris = selectedRecords.map((record) => record.assetUri).toList();
|
||||||
|
final thumbUris = selectedRecords.map((record) => record.thumbnailUri).toList();
|
||||||
|
|
||||||
|
final selectedUris = await showDialog<List<String>>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return StatefulBuilder(
|
return AlertDialog(
|
||||||
builder: (context, setState) {
|
icon: const Icon(Icons.download),
|
||||||
return AlertDialog(
|
title: const Text("Download what?"),
|
||||||
icon: const Icon(Icons.delete),
|
content: Column(
|
||||||
title: Text(iClient.selectedRecordCount == 1
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
? "Really delete this Record?"
|
mainAxisSize: MainAxisSize.min,
|
||||||
: "Really delete ${iClient.selectedRecordCount} Records?"),
|
children: [
|
||||||
content: const Text("This action cannot be undone!"),
|
const Divider(),
|
||||||
actionsAlignment: MainAxisAlignment.spaceBetween,
|
const SizedBox(
|
||||||
actions: [
|
height: 8,
|
||||||
TextButton(
|
),
|
||||||
onPressed: loading
|
TextButton.icon(
|
||||||
? null
|
onPressed: () {
|
||||||
: () {
|
Navigator.of(context).pop(assetUris);
|
||||||
Navigator.of(context).pop(false);
|
},
|
||||||
},
|
icon: const Icon(Icons.data_object),
|
||||||
child: const Text("Cancel"),
|
label: Text(
|
||||||
|
"Asset${iClient.selectedRecordCount != 1 ? "s" : ""} (${assetUris.map((e) => extension(e)).toList().unique().join(", ")})",
|
||||||
),
|
),
|
||||||
Row(
|
),
|
||||||
mainAxisSize: MainAxisSize.min,
|
TextButton.icon(
|
||||||
children: [
|
onPressed: () {
|
||||||
if (loading)
|
Navigator.of(context).pop(thumbUris);
|
||||||
const SizedBox.square(
|
},
|
||||||
dimension: 16,
|
icon: const Icon(Icons.image),
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
label: Text(
|
||||||
),
|
"Thumbnail${iClient.selectedRecordCount != 1 ? "s" : ""} (${thumbUris.map((e) => extension(e)).toList().unique().join(", ")})",
|
||||||
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"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
);
|
],
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
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(
|
const SizedBox(
|
||||||
width: 4,
|
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 '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/session_client.dart';
|
||||||
|
import 'package:contacts_plus_plus/clients/settings_client.dart';
|
||||||
import 'package:contacts_plus_plus/models/session.dart';
|
import 'package:contacts_plus_plus/models/session.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -44,9 +46,20 @@ class _SessionFilterDialogState extends State<SessionFilterDialog> {
|
||||||
super.dispose();
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
|
insetPadding: const EdgeInsets.all(24),
|
||||||
title: const Text("Filter"),
|
title: const Text("Filter"),
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
|
@ -109,7 +122,8 @@ class _SessionFilterDialogState extends State<SessionFilterDialog> {
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
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),
|
icon: const Icon(Icons.add_circle_outline),
|
||||||
|
@ -128,11 +142,13 @@ class _SessionFilterDialogState extends State<SessionFilterDialog> {
|
||||||
SessionFilterCheckbox(
|
SessionFilterCheckbox(
|
||||||
label: "Include Empty Headless",
|
label: "Include Empty Headless",
|
||||||
value: _currentFilter.includeEmptyHeadless && _currentFilter.minActiveUsers == 0,
|
value: _currentFilter.includeEmptyHeadless && _currentFilter.minActiveUsers == 0,
|
||||||
onChanged: _currentFilter.minActiveUsers > 0 ? null : (value) {
|
onChanged: _currentFilter.minActiveUsers > 0
|
||||||
setState(() {
|
? null
|
||||||
_currentFilter = _currentFilter.copyWith(includeEmptyHeadless: value);
|
: (value) {
|
||||||
});
|
setState(() {
|
||||||
},
|
_currentFilter = _currentFilter.copyWith(includeEmptyHeadless: value);
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
SessionFilterCheckbox(
|
SessionFilterCheckbox(
|
||||||
label: "Include Incompatible",
|
label: "Include Incompatible",
|
||||||
|
@ -155,9 +171,10 @@ class _SessionFilterDialogState extends State<SessionFilterDialog> {
|
||||||
child: const Text("Cancel"),
|
child: const Text("Cancel"),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
Provider.of<SessionClient>(context, listen: false).filterSettings = _currentFilter;
|
Provider.of<SessionClient>(context, listen: false).filterSettings = _currentFilter;
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
await _updateSettings();
|
||||||
},
|
},
|
||||||
child: const Text("Okay"),
|
child: const Text("Okay"),
|
||||||
),
|
),
|
||||||
|
|
|
@ -21,7 +21,7 @@ class _SessionListState extends State<SessionList> with AutomaticKeepAliveClient
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
final sClient = Provider.of<SessionClient>(context, listen: false);
|
final sClient = Provider.of<SessionClient>(context, listen: false);
|
||||||
if (sClient.sessionsFuture == null) {
|
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/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:contacts_plus_plus/widgets/sessions/session_filter_dialog.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -23,7 +25,7 @@ class _SessionListAppBarState extends State<SessionListAppBar> {
|
||||||
final sessionClient = Provider.of<SessionClient>(context, listen: false);
|
final sessionClient = Provider.of<SessionClient>(context, listen: false);
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => Provider.value(
|
builder: (context) => ChangeNotifierProvider.value(
|
||||||
value: sessionClient,
|
value: sessionClient,
|
||||||
child: SessionFilterDialog(
|
child: SessionFilterDialog(
|
||||||
lastFilter: sessionClient.filterSettings,
|
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
|
# 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
|
# 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.
|
# 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:
|
environment:
|
||||||
sdk: '>=3.0.1'
|
sdk: '>=3.0.1'
|
||||||
|
|
Loading…
Reference in a new issue