From a5331564830d2d45ac23fd449faa90c958d59cfa Mon Sep 17 00:00:00 2001 From: chetanr25 <1ds22ai010@dsce.edu.com> Date: Wed, 1 Jan 2025 01:15:50 +0530 Subject: [PATCH 1/5] Add Search Functionality and Fix Project List Display Bug --- .../components/search_text_field.dart | 33 ++++ .../components/search_toggle_button.dart | 28 +++ lib/ui/pages/landing_page/landing_page.dart | 176 +++++++++++------- 3 files changed, 174 insertions(+), 63 deletions(-) create mode 100644 lib/ui/pages/landing_page/components/search_text_field.dart create mode 100644 lib/ui/pages/landing_page/components/search_toggle_button.dart diff --git a/lib/ui/pages/landing_page/components/search_text_field.dart b/lib/ui/pages/landing_page/components/search_text_field.dart new file mode 100644 index 00000000..521cd075 --- /dev/null +++ b/lib/ui/pages/landing_page/components/search_text_field.dart @@ -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 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, + ); + } +} diff --git a/lib/ui/pages/landing_page/components/search_toggle_button.dart b/lib/ui/pages/landing_page/components/search_toggle_button.dart new file mode 100644 index 00000000..1cd08fc1 --- /dev/null +++ b/lib/ui/pages/landing_page/components/search_toggle_button.dart @@ -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, + ); + } +} diff --git a/lib/ui/pages/landing_page/landing_page.dart b/lib/ui/pages/landing_page/landing_page.dart index fa481f23..475e4c0f 100644 --- a/lib/ui/pages/landing_page/landing_page.dart +++ b/lib/ui/pages/landing_page/landing_page.dart @@ -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'; @@ -37,6 +39,18 @@ class _LandingPageState extends ConsumerState { 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> _getProjects() async { return database.projectDAO.getProjects(); } @@ -80,6 +94,14 @@ class _LandingPageState extends ConsumerState { } } + List _filterProjects(List 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); @@ -99,34 +121,63 @@ class _LandingPageState extends ConsumerState { 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> 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), @@ -146,21 +197,18 @@ class _LandingPageState extends ConsumerState { 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, ), ), ], @@ -174,36 +222,38 @@ class _LandingPageState extends ConsumerState { } }, ), - 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(); + }, + ), + ], + ), ); } } From 75b6324ad62c143f522747be2583fe5dc4b51141 Mon Sep 17 00:00:00 2001 From: chetanr25 <1ds22ai010@dsce.edu.com> Date: Sun, 19 Jan 2025 21:14:55 +0530 Subject: [PATCH 2/5] Sorting functionality added --- .gitignore | 2 + ios/Podfile.lock | 12 +- ios/Runner/AppDelegate.swift | 2 +- lib/core/models/sort_option.dart | 25 +++ .../components/search_text_field.dart | 85 ++++++++++ .../components/search_toggle_button.dart | 28 ++++ lib/ui/pages/landing_page/landing_page.dart | 147 ++++++++++++++---- pubspec.lock | 72 ++++++--- 8 files changed, 309 insertions(+), 64 deletions(-) create mode 100644 lib/core/models/sort_option.dart create mode 100644 lib/ui/pages/landing_page/components/search_text_field.dart create mode 100644 lib/ui/pages/landing_page/components/search_toggle_button.dart diff --git a/.gitignore b/.gitignore index 9642c494..8406c417 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 7cef8453..b315d352 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -121,18 +121,18 @@ SPEC CHECKSUMS: file_picker: ce3938a0df3cc1ef404671531facef740d03f920 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_localization: f43b18844a2b3d2c71fd64f04ffd6b1e64dd54d4 - image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 - integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 + image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 launch_review: 75d5a956ba8eaa493e9c9d4bf4c05e505e8d5ed0 package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 SDWebImage: dfe95b2466a9823cf9f0c6d01217c06550d7b29a - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 PODFILE CHECKSUM: 303789365c3a8d7bc562e5e65d7e8e15218ec5c6 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 858f2d68..c15652f9 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -2,7 +2,7 @@ import UIKit import Flutter import Photos -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/lib/core/models/sort_option.dart b/lib/core/models/sort_option.dart new file mode 100644 index 00000000..460efdc9 --- /dev/null +++ b/lib/core/models/sort_option.dart @@ -0,0 +1,25 @@ +enum SortOption { + nameAsc, + nameDesc, + dateModifiedNewest, + dateModifiedOldest, + dateCreatedNewest, + dateCreatedOldest; + + String get label { + switch (this) { + case SortOption.nameAsc: + return 'Name (A to Z)'; + case SortOption.nameDesc: + return 'Name (Z to A)'; + case SortOption.dateModifiedNewest: + return 'Last Modified (Newest)'; + case SortOption.dateModifiedOldest: + return 'Last Modified (Oldest)'; + case SortOption.dateCreatedNewest: + return 'Date Created (Newest)'; + case SortOption.dateCreatedOldest: + return 'Date Created (Oldest)'; + } + } +} diff --git a/lib/ui/pages/landing_page/components/search_text_field.dart b/lib/ui/pages/landing_page/components/search_text_field.dart new file mode 100644 index 00000000..ea8b6c5a --- /dev/null +++ b/lib/ui/pages/landing_page/components/search_text_field.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:paintroid/core/models/sort_option.dart'; +import 'package:paintroid/ui/theme/theme.dart'; + +class SearchTextField extends StatelessWidget { + final TextEditingController controller; + final FocusNode focusNode; + final ValueChanged onChanged; + final SortOption currentSortOption; + final ValueChanged onSortOptionSelected; + + const SearchTextField({ + super.key, + required this.controller, + required this.focusNode, + required this.onChanged, + required this.currentSortOption, + required this.onSortOptionSelected, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + 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, + ), + Divider( + color: + PaintroidTheme.of(context).onSurfaceColor.withOpacity(0.6), + ), + ], + ), + ), + PopupMenuButton( + icon: Icon( + Icons.sort, + color: PaintroidTheme.of(context).onSurfaceColor, + ), + tooltip: 'Sort options', + onSelected: onSortOptionSelected, + itemBuilder: (context) => SortOption.values + .map( + (option) => PopupMenuItem( + value: option, + child: Row( + children: [ + Icon( + option == currentSortOption + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + size: 18, + ), + const SizedBox(width: 8), + Text(option.label), + ], + ), + ), + ) + .toList(), + ), + const SizedBox(width: 2), + ], + ); + } +} diff --git a/lib/ui/pages/landing_page/components/search_toggle_button.dart b/lib/ui/pages/landing_page/components/search_toggle_button.dart new file mode 100644 index 00000000..1cd08fc1 --- /dev/null +++ b/lib/ui/pages/landing_page/components/search_toggle_button.dart @@ -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, + ); + } +} diff --git a/lib/ui/pages/landing_page/landing_page.dart b/lib/ui/pages/landing_page/landing_page.dart index fa481f23..07f180a7 100644 --- a/lib/ui/pages/landing_page/landing_page.dart +++ b/lib/ui/pages/landing_page/landing_page.dart @@ -19,9 +19,12 @@ 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'; +import 'package:paintroid/core/models/sort_option.dart'; class LandingPage extends ConsumerStatefulWidget { final String title; @@ -37,6 +40,20 @@ class _LandingPageState extends ConsumerState { late IFileService fileService; late IImageService imageService; + bool _isSearchActive = false; + String _searchQuery = ''; + final TextEditingController _searchController = TextEditingController(); + final FocusNode _searchFocusNode = FocusNode(); + bool _isSortByName = false; + SortOption _currentSortOption = SortOption.dateModifiedNewest; + + @override + void dispose() { + _searchController.dispose(); + _searchFocusNode.dispose(); + super.dispose(); + } + Future> _getProjects() async { return database.projectDAO.getProjects(); } @@ -80,6 +97,36 @@ class _LandingPageState extends ConsumerState { } } + List _filterProjects(List projects) { + List filteredProjects = projects; + + if (_searchQuery.isNotEmpty) { + filteredProjects = filteredProjects + .where((project) => + project.name.toLowerCase().contains(_searchQuery.toLowerCase())) + .toList(); + } + + filteredProjects.sort((a, b) { + switch (_currentSortOption) { + case SortOption.nameAsc: + return a.name.compareTo(b.name); + case SortOption.nameDesc: + return b.name.compareTo(a.name); + case SortOption.dateModifiedNewest: + return b.lastModified.compareTo(a.lastModified); + case SortOption.dateModifiedOldest: + return a.lastModified.compareTo(b.lastModified); + case SortOption.dateCreatedNewest: + return b.creationDate.compareTo(a.creationDate); + case SortOption.dateCreatedOldest: + return a.creationDate.compareTo(b.creationDate); + } + }); + + return filteredProjects; + } + @override Widget build(BuildContext context) { ToastContext().init(context); @@ -99,34 +146,70 @@ class _LandingPageState extends ConsumerState { 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; + }); + }, + currentSortOption: _currentSortOption, + onSortOptionSelected: (option) { + FocusScope.of(context).unfocus(); + setState(() { + _currentSortOption = option; + }); + }, + ) + : 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> 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), @@ -146,26 +229,24 @@ class _LandingPageState extends ConsumerState { 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, ), ), ], ); } else { + return SizedBox(); return Center( child: CircularProgressIndicator( backgroundColor: PaintroidTheme.of(context).fabBackgroundColor, diff --git a/pubspec.lock b/pubspec.lock index 41146529..01c51cd8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -292,10 +292,10 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" file_picker: dependency: "direct main" description: @@ -607,10 +607,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" io: dependency: transitive description: @@ -651,6 +651,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + url: "https://pub.dev" + source: hosted + version: "10.0.4" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" lints: dependency: transitive description: @@ -679,26 +703,26 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.12.0" mime: dependency: transitive description: @@ -751,10 +775,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_drawing: dependency: transitive description: @@ -871,10 +895,10 @@ packages: dependency: transitive description: name: platform - sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: @@ -895,10 +919,10 @@ packages: dependency: transitive description: name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "5.0.2" pub_semver: dependency: transitive description: @@ -1196,10 +1220,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" timing: dependency: transitive description: @@ -1316,10 +1340,10 @@ packages: dependency: transitive description: name: vm_service - sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "11.10.0" + version: "14.2.1" watcher: dependency: transitive description: @@ -1348,10 +1372,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" win32: dependency: transitive description: @@ -1393,5 +1417,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0 <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" From 03a1b22b05ef5c8c49dc0d861d3cbceed94ec402 Mon Sep 17 00:00:00 2001 From: chetanr25 <1ds22ai010@dsce.edu.com> Date: Sun, 19 Jan 2025 21:19:30 +0530 Subject: [PATCH 3/5] minor bug fix --- lib/ui/pages/landing_page/components/search_text_field.dart | 4 ---- lib/ui/pages/landing_page/landing_page.dart | 1 - 2 files changed, 5 deletions(-) diff --git a/lib/ui/pages/landing_page/components/search_text_field.dart b/lib/ui/pages/landing_page/components/search_text_field.dart index ea8b6c5a..9a23bc08 100644 --- a/lib/ui/pages/landing_page/components/search_text_field.dart +++ b/lib/ui/pages/landing_page/components/search_text_field.dart @@ -44,10 +44,6 @@ class SearchTextField extends StatelessWidget { ), onChanged: onChanged, ), - Divider( - color: - PaintroidTheme.of(context).onSurfaceColor.withOpacity(0.6), - ), ], ), ), diff --git a/lib/ui/pages/landing_page/landing_page.dart b/lib/ui/pages/landing_page/landing_page.dart index 07f180a7..88431f0c 100644 --- a/lib/ui/pages/landing_page/landing_page.dart +++ b/lib/ui/pages/landing_page/landing_page.dart @@ -246,7 +246,6 @@ class _LandingPageState extends ConsumerState { ], ); } else { - return SizedBox(); return Center( child: CircularProgressIndicator( backgroundColor: PaintroidTheme.of(context).fabBackgroundColor, From 5194560848b17e193a71481101eecbd2347ab304 Mon Sep 17 00:00:00 2001 From: chetanr25 <1ds22ai010@dsce.edu.com> Date: Sun, 26 Jan 2025 12:40:28 +0530 Subject: [PATCH 4/5] refactor: migrate landing page state to Riverpod & optimize loading UX --- .../object/search_filter_sort_provider.dart | 8 +++ lib/ui/pages/landing_page/landing_page.dart | 65 +++++++++---------- 2 files changed, 39 insertions(+), 34 deletions(-) create mode 100644 lib/core/providers/object/search_filter_sort_provider.dart diff --git a/lib/core/providers/object/search_filter_sort_provider.dart b/lib/core/providers/object/search_filter_sort_provider.dart new file mode 100644 index 00000000..8d12e6b3 --- /dev/null +++ b/lib/core/providers/object/search_filter_sort_provider.dart @@ -0,0 +1,8 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:paintroid/core/models/sort_option.dart'; + +final searchActiveProvider = StateProvider((ref) => false); +final searchQueryProvider = StateProvider((ref) => ''); +final sortOptionProvider = StateProvider( + (ref) => SortOption.dateModifiedNewest, +); diff --git a/lib/ui/pages/landing_page/landing_page.dart b/lib/ui/pages/landing_page/landing_page.dart index 6680f42b..d8f744fa 100644 --- a/lib/ui/pages/landing_page/landing_page.dart +++ b/lib/ui/pages/landing_page/landing_page.dart @@ -12,6 +12,7 @@ import 'package:paintroid/core/providers/object/image_service.dart'; import 'package:paintroid/core/providers/object/io_handler.dart'; import 'package:paintroid/core/providers/state/canvas_state_provider.dart'; import 'package:paintroid/core/providers/state/workspace_state_notifier.dart'; +import 'package:paintroid/core/providers/object/search_filter_sort_provider.dart'; import 'package:paintroid/core/utils/load_image_failure.dart'; import 'package:paintroid/core/utils/widget_identifier.dart'; import 'package:paintroid/ui/pages/landing_page/components/custom_action_button.dart'; @@ -40,13 +41,9 @@ class _LandingPageState extends ConsumerState { late IFileService fileService; late IImageService imageService; - bool _isSearchActive = false; - String _searchQuery = ''; final TextEditingController _searchController = TextEditingController(); final FocusNode _searchFocusNode = FocusNode(); - SortOption _currentSortOption = SortOption.dateModifiedNewest; - @override void dispose() { _searchController.dispose(); @@ -99,16 +96,18 @@ class _LandingPageState extends ConsumerState { List _filterProjects(List projects) { List filteredProjects = projects; + final searchQuery = ref.watch(searchQueryProvider); + final sortOption = ref.watch(sortOptionProvider); - if (_searchQuery.isNotEmpty) { + if (searchQuery.isNotEmpty) { filteredProjects = filteredProjects .where((project) => - project.name.toLowerCase().contains(_searchQuery.toLowerCase())) + project.name.toLowerCase().contains(searchQuery.toLowerCase())) .toList(); } filteredProjects.sort((a, b) { - switch (_currentSortOption) { + switch (sortOption) { case SortOption.nameAsc: return a.name.compareTo(b.name); case SortOption.nameDesc: @@ -143,50 +142,52 @@ class _LandingPageState extends ConsumerState { fileService = ref.watch(IFileService.provider); imageService = ref.watch(IImageService.provider); + final isSearchActive = ref.watch(searchActiveProvider); + final currentSortOption = ref.watch(sortOptionProvider); + return Scaffold( backgroundColor: PaintroidTheme.of(context).primaryColor, appBar: AppBar( - title: _isSearchActive + title: isSearchActive ? SearchTextField( controller: _searchController, focusNode: _searchFocusNode, onChanged: (value) { - setState(() { - _searchQuery = value; - }); + ref.read(searchQueryProvider.notifier).state = value; }, - currentSortOption: _currentSortOption, + currentSortOption: currentSortOption, onSortOptionSelected: (option) { - FocusScope.of(context).unfocus(); - setState(() { - _currentSortOption = option; - }); + ref.read(sortOptionProvider.notifier).state = option; }, - ) : Text(widget.title), actions: [ SearchToggleButton( - isSearchActive: _isSearchActive, + isSearchActive: isSearchActive, onSearchStart: () { - setState(() { - _isSearchActive = true; - }); + ref.read(searchActiveProvider.notifier).state = true; }, onSearchEnd: () { - setState(() { - _isSearchActive = false; - _searchQuery = ''; - _searchController.clear(); - }); + ref.read(searchActiveProvider.notifier).state = false; + ref.read(searchQueryProvider.notifier).state = ''; + _searchController.clear(); }, ), - if (!_isSearchActive) const MainOverflowMenu(), + if (!isSearchActive) const MainOverflowMenu(), ], ), body: FutureBuilder( future: _getProjects(), builder: (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.connectionState == ConnectionState.waiting && + !isSearchActive) { + return Center( + child: CircularProgressIndicator( + backgroundColor: PaintroidTheme.of(context).fabBackgroundColor, + ), + ); + } + if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { final filteredProjects = _filterProjects(snapshot.data!); @@ -195,7 +196,7 @@ class _LandingPageState extends ConsumerState { } return Column( children: [ - if (!_isSearchActive) + if (!isSearchActive) Flexible( flex: 2, child: _ProjectPreview( @@ -247,15 +248,11 @@ class _LandingPageState extends ConsumerState { ], ); } else { - return Center( - child: CircularProgressIndicator( - backgroundColor: PaintroidTheme.of(context).fabBackgroundColor, - ), - ); + return const SizedBox.shrink(); } }, ), - floatingActionButton: _isSearchActive + floatingActionButton: isSearchActive ? null : Column( mainAxisAlignment: MainAxisAlignment.end, From b655eea780ca579c85f708b51c0d2cce1f4c7398 Mon Sep 17 00:00:00 2001 From: chetanr25 <1ds22ai010@dsce.edu.com> Date: Fri, 31 Jan 2025 17:44:27 +0530 Subject: [PATCH 5/5] test: Add tests for search and sort functionality in landing page --- .../components/search_text_field.dart | 46 +++--- lib/ui/pages/landing_page/landing_page.dart | 3 + .../landing_page/landing_page_test.dart | 146 ++++++++++++++++++ 3 files changed, 171 insertions(+), 24 deletions(-) diff --git a/lib/ui/pages/landing_page/components/search_text_field.dart b/lib/ui/pages/landing_page/components/search_text_field.dart index ffcae7f8..b9ec8347 100644 --- a/lib/ui/pages/landing_page/components/search_text_field.dart +++ b/lib/ui/pages/landing_page/components/search_text_field.dart @@ -20,32 +20,26 @@ class SearchTextField extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( children: [ Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - 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, + child: 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, + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + ), + onChanged: onChanged, ), ), PopupMenuButton( @@ -68,14 +62,18 @@ class SearchTextField extends StatelessWidget { size: 18, ), const SizedBox(width: 8), - Text(option.label), + Flexible( + child: Text( + option.label, + overflow: TextOverflow.ellipsis, + ), + ), ], ), ), ) .toList(), ), - const SizedBox(width: 2), ], ); } diff --git a/lib/ui/pages/landing_page/landing_page.dart b/lib/ui/pages/landing_page/landing_page.dart index d8f744fa..d6954e7a 100644 --- a/lib/ui/pages/landing_page/landing_page.dart +++ b/lib/ui/pages/landing_page/landing_page.dart @@ -231,6 +231,9 @@ class _LandingPageState extends ConsumerState { flex: 3, child: ListView.builder( itemBuilder: (context, index) { + if (index == 0) { + return Container(); + } Project project = filteredProjects[index]; return ProjectListTile( project: project, diff --git a/test/widget/landing_page/landing_page_test.dart b/test/widget/landing_page/landing_page_test.dart index 91b910dd..eda68f46 100644 --- a/test/widget/landing_page/landing_page_test.dart +++ b/test/widget/landing_page/landing_page_test.dart @@ -23,6 +23,7 @@ import 'package:paintroid/core/providers/state/canvas_state_provider.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_text_field.dart'; import 'package:paintroid/ui/pages/workspace_page/components/top_bar/overflow_menu.dart'; import 'package:paintroid/ui/pages/workspace_page/components/top_bar/top_app_bar.dart'; import 'package:paintroid/ui/shared/dialogs/about_dialog.dart'; @@ -670,4 +671,149 @@ void main() { expect(find.byType(CircularProgressIndicator), findsOneWidget); }, ); + + testWidgets( + 'Should show search bar when search icon is tapped', + (tester) async { + when(database.projectDAO).thenReturn(dao); + when(dao.getProjects()).thenAnswer((_) => Future.value([])); + await tester.pumpWidget(sut); + await tester.pumpAndSettle(); + verify(database.projectDAO); + verify(dao.getProjects()); + + final searchIcon = find.byIcon(Icons.search); + expect(searchIcon, findsOneWidget); + + await tester.tap(searchIcon); + await tester.pumpAndSettle(); + + expect(find.byType(SearchTextField), findsOneWidget); + expect(find.byIcon(Icons.close), findsOneWidget); + expect(find.text('Search projects...'), findsOneWidget); + }, + ); + + testWidgets( + 'Should hide search bar when close icon is tapped', + (tester) async { + when(database.projectDAO).thenReturn(dao); + when(dao.getProjects()).thenAnswer((_) => Future.value([])); + await tester.pumpWidget(sut); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.search)); + await tester.pumpAndSettle(); + expect(find.byType(SearchTextField), findsOneWidget); + + await tester.tap(find.byIcon(Icons.close)); + await tester.pumpAndSettle(); + expect(find.byType(SearchTextField), findsNothing); + expect(find.text('Pocket Paint'), findsOneWidget); + }, + ); + + testWidgets( + 'Should filter projects based on search query', + (tester) async { + when(database.projectDAO).thenReturn(dao); + when(dao.getProjects()).thenAnswer((_) => Future.value(projects)); + when(imageService.getProjectPreview(filePath)) + .thenReturn(Result.ok(testFile.readAsBytesSync())); + await tester.pumpWidget(sut); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.search)); + await tester.pumpAndSettle(); + + await tester.enterText(find.byType(TextField), 'project1'); + await tester.pumpAndSettle(); + + expect(find.text('project1'), findsOneWidget); + expect(find.text('project2'), findsNothing); + }, + ); + + testWidgets( + 'Should show sort options menu when sort icon is tapped', + (tester) async { + when(database.projectDAO).thenReturn(dao); + when(dao.getProjects()).thenAnswer((_) => Future.value([])); + await tester.pumpWidget(sut); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.search)); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.sort)); + await tester.pumpAndSettle(); + + expect(find.text('Name (A to Z)'), findsOneWidget); + expect(find.text('Name (Z to A)'), findsOneWidget); + expect(find.text('Last Modified (Newest)'), findsOneWidget); + expect(find.text('Last Modified (Oldest)'), findsOneWidget); + expect(find.text('Date Created (Newest)'), findsOneWidget); + expect(find.text('Date Created (Oldest)'), findsOneWidget); + }, + ); + + testWidgets( + 'Should sort projects by name when name sort option is selected', + (tester) async { + final projectsToSort = [ + createProject('Project first'), + createProject('Project second'), + createProject('Project third'), + ]; + + when(database.projectDAO).thenReturn(dao); + when(deviceService.getSizeInPixels()) + .thenAnswer((_) => Future.value(const Size(1080, 1920))); + when(dao.getProjects()).thenAnswer((_) => Future.value(projectsToSort)); + when(imageService.getProjectPreview(filePath)) + .thenReturn(Result.ok(testFile.readAsBytesSync())); + await tester.pumpWidget(sut); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.search)); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.sort)); + await tester.pumpAndSettle(); + await tester.tap(find.text('Name (A to Z)')); + await tester.pumpAndSettle(); + + final projectNames = tester + .widgetList(find.byType(Text)) + .map((widget) => widget.data) + .where((text) => + text == 'Project first' || + text == 'Project second' || + text == 'Project third') + .toList(); + + expect(projectNames[0], equals('Project second')); + expect(projectNames[1], equals('Project third')); + }, + ); + + testWidgets( + 'Should hide floating action buttons when search is active', + (tester) async { + when(database.projectDAO).thenReturn(dao); + when(dao.getProjects()).thenAnswer((_) => Future.value([])); + await tester.pumpWidget(sut); + await tester.pumpAndSettle(); + + expect(find.byType(FloatingActionButton), findsNWidgets(2)); + + await tester.tap(find.byIcon(Icons.search)); + await tester.pumpAndSettle(); + + expect(find.byType(FloatingActionButton), findsNothing); + await tester.tap(find.byIcon(Icons.close)); + await tester.pumpAndSettle(); + expect(find.byType(FloatingActionButton), findsNWidgets(2)); + }, + ); }