Skip to content

Commit

Permalink
refactor(neon_framework): Decouple nested data in UnifiedSearchBloc
Browse files Browse the repository at this point in the history
Signed-off-by: provokateurin <kate@provokateurin.de>
  • Loading branch information
provokateurin committed Feb 19, 2024
1 parent 9bd30e6 commit fb507d8
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 124 deletions.
116 changes: 64 additions & 52 deletions packages/neon_framework/lib/src/blocs/unified_search.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import 'dart:async';

import 'package:built_collection/built_collection.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:meta/meta.dart';
import 'package:neon_framework/models.dart';
import 'package:neon_framework/src/bloc/bloc.dart';
import 'package:neon_framework/src/bloc/result.dart';
import 'package:neon_framework/src/blocs/apps.dart';
import 'package:neon_framework/src/utils/request_manager.dart';
import 'package:nextcloud/core.dart' as core;
import 'package:rxdart/rxdart.dart';

Expand Down Expand Up @@ -34,8 +36,11 @@ sealed class UnifiedSearchBloc implements InteractiveBloc {
/// Contains whether unified search is currently enabled.
BehaviorSubject<bool> get enabled;

/// The available search providers.
BehaviorSubject<Result<BuiltList<core.UnifiedSearchProvider>>> get providers;

/// Contains the unified search results mapped by provider.
BehaviorSubject<Result<Map<core.UnifiedSearchProvider, Result<core.UnifiedSearchResult>>?>> get results;
BehaviorSubject<BuiltMap<String, Result<core.UnifiedSearchResult>>> get results;
}

class _UnifiedSearchBloc extends InteractiveBloc implements UnifiedSearchBloc {
Expand All @@ -48,6 +53,19 @@ class _UnifiedSearchBloc extends InteractiveBloc implements UnifiedSearchBloc {
disable();
}
});

providers.listen((result) async {
if (result.isLoading) {
return;
}

if (term.isEmpty) {
results.add(BuiltMap());
return;
}

await searchProviders(result.requireData.map((provider) => provider.id).toList());
});
}

final AppsBloc appsBloc;
Expand All @@ -58,37 +76,28 @@ class _UnifiedSearchBloc extends InteractiveBloc implements UnifiedSearchBloc {
final enabled = BehaviorSubject.seeded(false);

@override
final results = BehaviorSubject.seeded(Result.success(null));
final providers = BehaviorSubject.seeded(Result.success(BuiltList()));

@override
final results = BehaviorSubject.seeded(BuiltMap());

@override
void dispose() {
unawaited(enabled.close());
unawaited(providers.close());
unawaited(results.close());
super.dispose();
}

@override
Future<void> refresh() async {
if (term.isEmpty) {
results.add(Result.success(null));
return;
}

try {
results.add(results.value.asLoading());
final response = await account.client.core.unifiedSearch.getProviders();
final providers = response.body.ocs.data;
results.add(
Result.success(Map.fromEntries(getLoadingProviders(providers))),
);
for (final provider in providers) {
unawaited(searchProvider(provider));
}
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());
results.add(Result.error(e));
}
await RequestManager.instance.wrapNextcloud(
account: account,
cacheKey: 'unified-search-providers',
subject: providers,
rawResponse: account.client.core.unifiedSearch.getProvidersRaw(),
unwrap: (response) => response.body.ocs.data,
);
}

@override
Expand All @@ -105,49 +114,52 @@ class _UnifiedSearchBloc extends InteractiveBloc implements UnifiedSearchBloc {
@override
void disable() {
enabled.add(false);
results.add(Result.success(null));
results.add(BuiltMap());
term = '';
}

Iterable<MapEntry<core.UnifiedSearchProvider, Result<core.UnifiedSearchResult>>> getLoadingProviders(
Iterable<core.UnifiedSearchProvider> providers,
) sync* {
for (final provider in providers) {
yield MapEntry(provider, Result.loading());
}
}

Future<void> searchProvider(core.UnifiedSearchProvider provider) async {
updateResults(provider, Result.loading());
try {
final response = await account.client.core.unifiedSearch.search(
providerId: provider.id,
term: term,
);
updateResults(provider, Result.success(response.body.ocs.data));
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());
updateResults(provider, Result.error(e));
}
Future<void> searchProviders(List<String> providerIDs) async {
results.add(
BuiltMap({
for (final providerID in providerIDs) providerID: Result<core.UnifiedSearchResult>.loading(),
}),
);

await Future.wait(
providerIDs.map((providerID) async {
try {
final response = await account.client.core.unifiedSearch.search(
providerId: providerID,
term: term,
);
updateResults(providerID, Result.success(response.body.ocs.data));
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());
updateResults(providerID, Result.error(e));
}
}),
);
}

void updateResults(core.UnifiedSearchProvider provider, Result<core.UnifiedSearchResult> result) => results.add(
Result.success(
void updateResults(String providerID, Result<core.UnifiedSearchResult> result) => results.add(
BuiltMap(
Map.fromEntries(
sortResults({
...?results.value.data,
provider: result,
...results.value.toMap(),
providerID: result,
}),
),
),
);

Iterable<MapEntry<core.UnifiedSearchProvider, Result<core.UnifiedSearchResult>>> sortResults(
Map<core.UnifiedSearchProvider, Result<core.UnifiedSearchResult>> results,
Iterable<MapEntry<String, Result<core.UnifiedSearchResult>>> sortResults(
Map<String, Result<core.UnifiedSearchResult>> results,
) sync* {
final activeApp = appsBloc.activeApp.value;

// Unlike non-matching providers (below) we don't filter the empty results,
// as the active app is more relevant and we want to know if there are no results for the active app.
yield* results.entries
.where((entry) => providerMatchesApp(entry.key, activeApp))
.sorted((a, b) => sortEntriesCount(a.value, b.value));
Expand All @@ -157,8 +169,8 @@ class _UnifiedSearchBloc extends InteractiveBloc implements UnifiedSearchBloc {
.sorted((a, b) => sortEntriesCount(a.value, b.value));
}

bool providerMatchesApp(core.UnifiedSearchProvider provider, AppImplementation app) =>
provider.id == app.id || provider.id.startsWith('${app.id}_');
bool providerMatchesApp(String providerID, AppImplementation app) =>
providerID == app.id || providerID.startsWith('${app.id}_');

bool hasEntries(Result<core.UnifiedSearchResult> result) => !result.hasData || result.requireData.entries.isNotEmpty;

Expand Down
142 changes: 70 additions & 72 deletions packages/neon_framework/lib/src/widgets/unified_search_results.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:built_collection/built_collection.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:meta/meta.dart';
Expand Down Expand Up @@ -27,32 +28,20 @@ class NeonUnifiedSearchResults extends StatelessWidget {
final accountsBloc = NeonProvider.of<AccountsBloc>(context);
final bloc = accountsBloc.activeUnifiedSearchBloc;
return ResultBuilder.behaviorSubject(
subject: bloc.results,
builder: (context, results) {
final values = results.data?.entries.toList();

return NeonListView(
scrollKey: 'unified-search',
isLoading: results.isLoading,
error: results.error,
onRefresh: bloc.refresh,
itemCount: values?.length ?? 0,
itemBuilder: (context, index) {
final snapshot = values![index];

return AnimatedSize(
duration: const Duration(milliseconds: 100),
child: _buildProvider(
context,
accountsBloc,
bloc,
snapshot.key,
snapshot.value,
),
);
},
);
},
subject: bloc.providers,
builder: (context, providers) => NeonListView(
scrollKey: 'unified-search',
isLoading: providers.isLoading,
error: providers.error,
onRefresh: bloc.refresh,
itemCount: providers.data?.length ?? 0,
itemBuilder: (context, index) => _buildProvider(
context,
accountsBloc,
bloc,
providers.requireData[index],
),
),
);
}

Expand All @@ -61,53 +50,62 @@ class NeonUnifiedSearchResults extends StatelessWidget {
AccountsBloc accountsBloc,
UnifiedSearchBloc bloc,
core.UnifiedSearchProvider provider,
Result<core.UnifiedSearchResult> result,
) {
final entries = result.data?.entries ?? <core.UnifiedSearchResultEntry>[];
return Card(
child: Container(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
provider.name,
style: Theme.of(context).textTheme.headlineSmall,
),
NeonError(
result.error,
onRetry: bloc.refresh,
),
NeonLinearProgressIndicator(
visible: result.isLoading,
),
if (entries.isEmpty) ...[
AdaptiveListTile(
leading: const Icon(
Icons.close,
size: largeIconSize,
),
title: Text(NeonLocalizations.of(context).searchNoResults),
),
],
for (final entry in entries) ...[
AdaptiveListTile(
leading: NeonImageWrapper(
size: const Size.square(largeIconSize),
child: _buildThumbnail(context, accountsBloc.activeAccount.value!, entry),
),
title: Text(entry.title),
subtitle: Text(entry.subline),
onTap: () async {
context.go(entry.resourceUrl);
},
) =>
StreamBuilder(
stream: bloc.results.map((results) => results[provider.id]),
builder: (context, snapshot) {
final result = snapshot.data;
if (result == null) {
return const SizedBox.shrink();
}

final entries = result.data?.entries ?? BuiltList<core.UnifiedSearchResultEntry>();

return Card(
child: Container(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
provider.name,
style: Theme.of(context).textTheme.headlineSmall,
),
NeonError(
result.error,
onRetry: bloc.refresh,
),
NeonLinearProgressIndicator(
visible: result.isLoading,
),
if (!result.isLoading && entries.isEmpty) ...[
AdaptiveListTile(
leading: const Icon(
Icons.close,
size: largeIconSize,
),
title: Text(NeonLocalizations.of(context).searchNoResults),
),
],
for (final entry in entries) ...[
AdaptiveListTile(
leading: NeonImageWrapper(
size: const Size.square(largeIconSize),
child: _buildThumbnail(context, accountsBloc.activeAccount.value!, entry),
),
title: Text(entry.title),
subtitle: Text(entry.subline),
onTap: () async {
context.go(entry.resourceUrl);
},
),
],
],
),
],
],
),
),
);
}
),
);
},
);

Widget _buildThumbnail(BuildContext context, Account account, core.UnifiedSearchResultEntry entry) {
if (entry.thumbnailUrl.isNotEmpty) {
Expand Down

0 comments on commit fb507d8

Please sign in to comment.