Skip to content

Commit

Permalink
feat: refactor Search to widget and make tags clickable
Browse files Browse the repository at this point in the history
  • Loading branch information
johannesvedder committed Jul 4, 2023
1 parent 5acfa04 commit 1d6d332
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 72 deletions.
76 changes: 76 additions & 0 deletions designer_v2/lib/common_views/search.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:studyu_designer_v2/theme.dart';

class Search extends StatefulWidget {
final Function(String) onQueryChanged;
final SearchController? searchController;
final String? hintText;
final String? initialText;

const Search({
super.key,
required this.onQueryChanged,
this.searchController,
this.hintText,
this.initialText,
});

@override
SearchState createState() => SearchState();
}

class SearchState extends State<Search> {
late TextEditingController _searchController;

@override
void initState() {
super.initState();
widget.searchController?.setText = setText;
_searchController = TextEditingController(text: widget.initialText);
_searchController.addListener(_onSearchPressed);
}

@override
void didUpdateWidget(Search oldWidget) {
super.didUpdateWidget(oldWidget);
}

void _onSearchPressed() {
String query = _searchController.text.toLowerCase();
widget.onQueryChanged(query);
}

void setText(String text) {
setState(() {
_searchController.text = text;
});
}

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SizedBox(
width: 400.0,
child: SearchBar(
hintText: widget.hintText ?? "Search",
controller: _searchController,
leading: const Icon(Icons.search),
backgroundColor: MaterialStateProperty.resolveWith((states) {
return ThemeConfig.sidesheetBackgroundColor(theme).withOpacity(0.5);
}),
)
);
}

@override
void dispose() {
super.dispose();
_searchController.removeListener(_onSearchPressed);
_searchController.dispose();
}
}

class SearchController {
late void Function(String text) setText;

}
21 changes: 13 additions & 8 deletions designer_v2/lib/features/dashboard/dashboard_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:studyu_core/core.dart';
import 'package:studyu_designer_v2/common_views/search.dart';
import 'package:studyu_designer_v2/domain/study.dart';
import 'package:studyu_designer_v2/features/dashboard/studies_filter.dart';
import 'package:studyu_designer_v2/features/study/study_actions.dart';
Expand All @@ -27,6 +28,8 @@ class DashboardController extends StateNotifier<DashboardState> implements IMode
/// A subscription for synchronizing state between the repository and the controller
StreamSubscription<List<WrappedModel<Study>>>? _studiesSubscription;

final SearchController searchController = SearchController();

DashboardController({
required this.studyRepository,
required this.authRepository,
Expand All @@ -51,6 +54,10 @@ class DashboardController extends StateNotifier<DashboardState> implements IMode
});
}

setSearchText(String? text) {
searchController.setText(text ?? state.query);
}

setStudiesFilter(StudiesFilter? filter) {
state = state.copyWith(studiesFilter: () => filter ?? DashboardState.defaultFilter);
}
Expand All @@ -63,14 +70,6 @@ class DashboardController extends StateNotifier<DashboardState> implements IMode
router.dispatch(RoutingIntents.studyNew);
}

String? search(String query) {
if (query.isEmpty) {
return null;
} else {
return query.toLowerCase();
}
}

Future<void> pinStudy(String modelId) async {
// todo move to userRepository [updatePreferences]
final newPinnedStudies = Set<String>.from(userRepository.user.preferences.pinnedStudies);
Expand All @@ -89,6 +88,12 @@ class DashboardController extends StateNotifier<DashboardState> implements IMode
sortStudies();
}

void filterStudies(String? query) async {
state = state.copyWith(
query: query,
);
}

void sortStudies() async {
final studies = state.sort(pinnedStudies: userRepository.user.preferences.pinnedStudies);
state = state.copyWith(
Expand Down
41 changes: 10 additions & 31 deletions designer_v2/lib/features/dashboard/dashboard_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import 'package:studyu_core/core.dart';
import 'package:studyu_designer_v2/common_views/async_value_widget.dart';
import 'package:studyu_designer_v2/common_views/empty_body.dart';
import 'package:studyu_designer_v2/common_views/primary_button.dart';
import 'package:studyu_designer_v2/common_views/search.dart';
import 'package:studyu_designer_v2/features/dashboard/dashboard_controller.dart';
import 'package:studyu_designer_v2/features/dashboard/dashboard_scaffold.dart';
import 'package:studyu_designer_v2/features/dashboard/dashboard_state.dart';
import 'package:studyu_designer_v2/features/dashboard/studies_filter.dart';
import 'package:studyu_designer_v2/features/dashboard/studies_table.dart';
import 'package:studyu_designer_v2/localization/app_translation.dart';
import 'package:studyu_designer_v2/repositories/user_repository.dart';
import 'package:studyu_designer_v2/theme.dart';
import 'package:studyu_designer_v2/utils/performance.dart';

class DashboardScreen extends ConsumerStatefulWidget {
Expand All @@ -26,17 +26,13 @@ class DashboardScreen extends ConsumerStatefulWidget {
class _DashboardScreenState extends ConsumerState<DashboardScreen> {
late final DashboardController controller;
late final DashboardState state;
late final Future<Preferences> preferences;
final searchController = TextEditingController();
String? searchQuery;

@override
void initState() {
super.initState();
controller = ref.read(dashboardControllerProvider.notifier);
state = ref.read(dashboardControllerProvider);
runAsync(() => controller.setStudiesFilter(widget.filter));
searchController.addListener(searchListener);
}

@override
Expand All @@ -47,18 +43,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
}
}

@override
void dispose() {
super.dispose();
searchController.removeListener(searchListener);
}

void searchListener() {
setState(() {
searchQuery = controller.search(searchController.text);
});
}

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
Expand All @@ -81,34 +65,28 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
const SizedBox(width: 28.0),
SelectableText(state.visibleListTitle, style: theme.textTheme.headlineMedium),
const Spacer(),
SizedBox(
width: 400.0,
child: SearchBar(
Search(
searchController: controller.searchController,
hintText: tr.search,
controller: searchController,
leading: const Icon(Icons.search),
backgroundColor: MaterialStateProperty.resolveWith((states) {
return ThemeConfig.sidesheetBackgroundColor(theme).withOpacity(0.5);
}),
),
onQueryChanged: (query) => controller.filterStudies(query)
),
],
),
const SizedBox(height: 24.0), // spacing between body elements
FutureBuilder<StudyUUser>(
future: userRepo.fetchUser(),
future: userRepo.fetchUser(), // todo cache this with ModelRepository
builder: (context, snapshot) {
if (snapshot.hasData) {
return AsyncValueWidget<List<Study>>(
value: state.visibleStudies(searchQuery, snapshot.data!.preferences.pinnedStudies),
value: state.visibleStudies(snapshot.data!.preferences.pinnedStudies, state.query),
data: (visibleStudies) => StudiesTable(
studies: visibleStudies,
pinnedStudies: snapshot.data!.preferences.pinnedStudies,
dashboardProvider: ref.read(dashboardControllerProvider.notifier),
dashboardController: ref.read(dashboardControllerProvider.notifier),
onSelect: controller.onSelectStudy,
getActions: controller.availableActions,
emptyWidget: (widget.filter == null || widget.filter == StudiesFilter.owned)
? (searchQuery != null && searchQuery!.isNotEmpty)
? (state.query.isNotEmpty)
? Padding(
padding: const EdgeInsets.only(top: 24.0),
child: EmptyBody(
Expand All @@ -128,7 +106,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
),
)
: const SizedBox.shrink(),
));
)
);
}
return const SizedBox.shrink();
}),
Expand Down
19 changes: 16 additions & 3 deletions designer_v2/lib/features/dashboard/dashboard_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class DashboardState extends Equatable {
const DashboardState({
this.studies = const AsyncValue.loading(),
this.studiesFilter = defaultFilter,
this.query = '',
required this.currentUser,
});

Expand All @@ -25,15 +26,18 @@ class DashboardState extends Equatable {
/// Currently authenticated user (used for filtering studies)
final User currentUser;

final String query;

/// The currently visible list of studies as by the selected filter
///
/// Wrapped in an [AsyncValue] that mirrors the [studies]' async states,
/// but resolves to a different subset of studies based on the [studiesFilter]
AsyncValue<List<Study>> visibleStudies(String? query, Set<String> pinnedStudies) {
AsyncValue<List<Study>> visibleStudies(Set<String> pinnedStudies, String query) {
return studies.when(
data: (studies) {
List<Study> updatedStudies =
studiesFilter.apply(unfilteredStudies: studies, user: currentUser, query: query).toList();
studiesFilter.apply(studies: studies, user: currentUser).toList();
updatedStudies = filter(studiesToFilter: updatedStudies);
updatedStudies = sort(pinnedStudies: pinnedStudies, studiesToSort: updatedStudies);
return AsyncValue.data(updatedStudies);
},
Expand All @@ -42,6 +46,14 @@ class DashboardState extends Equatable {
);
}

List<Study> filter({List<Study>? studiesToFilter}) {
final filteredStudies = studiesToFilter ?? studies.value!;
if (query.isNotEmpty) {
return filteredStudies.where((s) => s.title!.toLowerCase().contains(query)).toList();
}
return filteredStudies;
}

List<Study> sort({required Set<String> pinnedStudies, List<Study>? studiesToSort}) {
final sortedStudies = studiesToSort ?? studies.value!;
sortedStudies.sort((study, other) => study.title!.compareTo(other.title!));
Expand All @@ -66,12 +78,13 @@ class DashboardState extends Equatable {
AsyncValue<List<Study>> Function()? studies,
StudiesFilter Function()? studiesFilter,
User Function()? currentUser,
AsyncValue<Set<String>> Function()? pinnedStudies,
String? query,
}) {
return DashboardState(
studies: studies != null ? studies() : this.studies,
studiesFilter: studiesFilter != null ? studiesFilter() : this.studiesFilter,
currentUser: currentUser != null ? currentUser() : this.currentUser,
query: query ?? this.query,
);
}

Expand Down
6 changes: 1 addition & 5 deletions designer_v2/lib/features/dashboard/studies_filter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ import 'package:supabase_flutter/supabase_flutter.dart';
enum StudiesFilter with GoRouteParamEnum { all, owned, shared, public }

extension StudiesFilterByUser on StudiesFilter {
Iterable<Study> apply({required Iterable<Study> unfilteredStudies, required User user, String? query}) {
Iterable<Study> studies = unfilteredStudies;
if (query != null && query.isNotEmpty) {
studies = unfilteredStudies.where((s) => s.title!.toLowerCase().contains(query));
}
Iterable<Study> apply({required Iterable<Study> studies, required User user}) {
switch (this) {
case StudiesFilter.all:
return studies;
Expand Down
Loading

0 comments on commit 1d6d332

Please sign in to comment.