Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PAINTROID-490: Search bar on Landing Page #105

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions lib/ui/pages/landing_page/components/search_text_field.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:paintroid/ui/theme/theme.dart';

class SearchTextField extends StatelessWidget {
final TextEditingController controller;
final FocusNode focusNode;
final ValueChanged<String> onChanged;

const SearchTextField({
super.key,
required this.controller,
required this.focusNode,
required this.onChanged,
});

@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
focusNode: focusNode,
autofocus: true,
style: TextStyle(color: PaintroidTheme.of(context).onSurfaceColor),
decoration: InputDecoration(
hintText: 'Search projects...',
hintStyle: TextStyle(
color: PaintroidTheme.of(context).onSurfaceColor.withOpacity(0.6),
),
border: InputBorder.none,
),
onChanged: onChanged,
);
}
}
28 changes: 28 additions & 0 deletions lib/ui/pages/landing_page/components/search_toggle_button.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:flutter/material.dart';

class SearchToggleButton extends StatelessWidget {
final bool isSearchActive;
final VoidCallback onSearchStart;
final VoidCallback onSearchEnd;

const SearchToggleButton({
super.key,
required this.isSearchActive,
required this.onSearchStart,
required this.onSearchEnd,
});

@override
Widget build(BuildContext context) {
if (isSearchActive) {
return IconButton(
icon: const Icon(Icons.close),
onPressed: onSearchEnd,
);
}
return IconButton(
icon: const Icon(Icons.search),
onPressed: onSearchStart,
);
}
}
176 changes: 113 additions & 63 deletions lib/ui/pages/landing_page/landing_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import 'package:paintroid/ui/pages/landing_page/components/image_preview.dart';
import 'package:paintroid/ui/pages/landing_page/components/main_overflow_menu.dart';
import 'package:paintroid/ui/pages/landing_page/components/project_list_tile.dart';
import 'package:paintroid/ui/pages/landing_page/components/project_overflow_menu.dart';
import 'package:paintroid/ui/pages/landing_page/components/search_toggle_button.dart';
import 'package:paintroid/ui/pages/landing_page/components/search_text_field.dart';
import 'package:paintroid/ui/shared/icon_svg.dart';
import 'package:paintroid/ui/theme/theme.dart';
import 'package:paintroid/ui/utils/toast_utils.dart';
Expand All @@ -37,6 +39,18 @@ class _LandingPageState extends ConsumerState<LandingPage> {
late IFileService fileService;
late IImageService imageService;

bool _isSearchActive = false;
String _searchQuery = '';
final TextEditingController _searchController = TextEditingController();
final FocusNode _searchFocusNode = FocusNode();

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

Future<List<Project>> _getProjects() async {
return database.projectDAO.getProjects();
}
Expand Down Expand Up @@ -80,6 +94,14 @@ class _LandingPageState extends ConsumerState<LandingPage> {
}
}

List<Project> _filterProjects(List<Project> projects) {
if (_searchQuery.isEmpty) return projects;
return projects
.where((project) =>
project.name.toLowerCase().contains(_searchQuery.toLowerCase()))
.toList();
}

@override
Widget build(BuildContext context) {
ToastContext().init(context);
Expand All @@ -99,34 +121,63 @@ class _LandingPageState extends ConsumerState<LandingPage> {
return Scaffold(
backgroundColor: PaintroidTheme.of(context).primaryColor,
appBar: AppBar(
title: Text(widget.title),
actions: const [MainOverflowMenu()],
title: _isSearchActive
? SearchTextField(
controller: _searchController,
focusNode: _searchFocusNode,
onChanged: (value) {
setState(() {
_searchQuery = value;
});
},
)
: Text(widget.title),
actions: [
SearchToggleButton(
isSearchActive: _isSearchActive,
onSearchStart: () {
setState(() {
_isSearchActive = true;
});
},
onSearchEnd: () {
setState(() {
_isSearchActive = false;
_searchQuery = '';
_searchController.clear();
});
},
),
if (!_isSearchActive) const MainOverflowMenu(),
],
),
body: FutureBuilder(
future: _getProjects(),
builder: (BuildContext context, AsyncSnapshot<List<Project>> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasData) {
if (snapshot.data!.isNotEmpty) {
latestModifiedProject = snapshot.data![0];
final filteredProjects = _filterProjects(snapshot.data!);
if (filteredProjects.isNotEmpty) {
latestModifiedProject = filteredProjects[0];
}
return Column(
children: [
Flexible(
flex: 2,
child: _ProjectPreview(
ioHandler: ioHandler,
imageService: imageService,
latestModifiedProject: latestModifiedProject,
onProjectPreviewTap: () {
if (latestModifiedProject != null) {
_openProject(latestModifiedProject, ioHandler, ref);
} else {
_clearCanvas();
_navigateToPocketPaint();
}
}),
),
if (!_isSearchActive)
Flexible(
flex: 2,
child: _ProjectPreview(
ioHandler: ioHandler,
imageService: imageService,
latestModifiedProject: latestModifiedProject,
onProjectPreviewTap: () {
if (latestModifiedProject != null) {
_openProject(latestModifiedProject, ioHandler, ref);
} else {
_clearCanvas();
_navigateToPocketPaint();
}
}),
),
Container(
color: PaintroidTheme.of(context).primaryContainerColor,
padding: const EdgeInsets.all(20),
Expand All @@ -146,21 +197,18 @@ class _LandingPageState extends ConsumerState<LandingPage> {
flex: 3,
child: ListView.builder(
itemBuilder: (context, index) {
if (index != 0) {
Project project = snapshot.data![index];
return ProjectListTile(
project: project,
imageService: imageService,
index: index,
onTap: () async {
_clearCanvas();
_openProject(project, ioHandler, ref);
},
);
}
return Container();
Project project = filteredProjects[index];
return ProjectListTile(
project: project,
imageService: imageService,
index: index,
onTap: () async {
_clearCanvas();
_openProject(project, ioHandler, ref);
},
);
},
itemCount: snapshot.data?.length,
itemCount: filteredProjects.length,
),
),
],
Expand All @@ -174,36 +222,38 @@ class _LandingPageState extends ConsumerState<LandingPage> {
}
},
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
CustomActionButton(
heroTag: 'import_image',
icon: Icons.file_download,
hint: 'Load image',
onPressed: () async {
final bool imageLoaded =
await ioHandler.loadImage(context, this, false);
if (imageLoaded && mounted) {
_navigateToPocketPaint();
}
},
),
const SizedBox(
height: 10,
),
CustomActionButton(
key: const ValueKey(WidgetIdentifier.newImageActionButton),
heroTag: 'new_image',
icon: Icons.add,
hint: 'New image',
onPressed: () async {
_clearCanvas();
_navigateToPocketPaint();
},
),
],
),
floatingActionButton: _isSearchActive
? null
: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
CustomActionButton(
heroTag: 'import_image',
icon: Icons.file_download,
hint: 'Load image',
onPressed: () async {
final bool imageLoaded =
await ioHandler.loadImage(context, this, false);
if (imageLoaded && mounted) {
_navigateToPocketPaint();
}
},
),
const SizedBox(
height: 10,
),
CustomActionButton(
key: const ValueKey(WidgetIdentifier.newImageActionButton),
heroTag: 'new_image',
icon: Icons.add,
hint: 'New image',
onPressed: () async {
_clearCanvas();
_navigateToPocketPaint();
},
),
],
),
);
}
}
Expand Down