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/8] 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/8] 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/8] 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/8] 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/8] 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)); + }, + ); } From 9fd12e13d85fa7cc54ef22ad6cd98ad13c964190 Mon Sep 17 00:00:00 2001 From: Lenkomotive Date: Thu, 30 Oct 2025 15:48:56 +0100 Subject: [PATCH 6/8] PAINTROID-454: Flutter: Add Layers --- devtools_options.yaml | 3 + ios/Podfile.lock | 43 +-- ...ayers_panel_visibility_state_provider.dart | 24 ++ ...ers_panel_visibility_state_provider.g.dart | 27 ++ .../components/bottom_bar/bottom_nav_bar.dart | 8 + .../components/layers_panel/layers_panel.dart | 360 ++++++++++++++++++ .../pages/workspace_page/workspace_page.dart | 2 + 7 files changed, 445 insertions(+), 22 deletions(-) create mode 100644 devtools_options.yaml create mode 100644 lib/core/providers/state/layers_panel_visibility_state_provider.dart create mode 100644 lib/core/providers/state/layers_panel_visibility_state_provider.g.dart create mode 100644 lib/ui/pages/workspace_page/components/layers_panel/layers_panel.dart diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 00000000..fa0b357c --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/ios/Podfile.lock b/ios/Podfile.lock index cf7a5a73..a2aa913b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -42,14 +42,14 @@ PODS: - Flutter - integration_test (0.0.1): - Flutter - - launch_review (0.0.1): + - launch_review_latest (0.0.1): - Flutter - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - permission_handler_apple (9.1.1): + - permission_handler_apple (9.3.0): - Flutter - SDWebImage (5.19.2): - SDWebImage/Core (= 5.19.2) @@ -57,7 +57,7 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sqflite (0.0.3): + - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS - SwiftyGif (5.4.5) @@ -71,12 +71,12 @@ DEPENDENCIES: - flutter_localization (from `.symlinks/plugins/flutter_localization/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - - launch_review (from `.symlinks/plugins/launch_review/ios`) + - launch_review_latest (from `.symlinks/plugins/launch_review_latest/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite (from `.symlinks/plugins/sqflite/darwin`) + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: @@ -99,8 +99,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/image_picker_ios/ios" integration_test: :path: ".symlinks/plugins/integration_test/ios" - launch_review: - :path: ".symlinks/plugins/launch_review/ios" + launch_review_latest: + :path: ".symlinks/plugins/launch_review_latest/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: @@ -109,31 +109,30 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/permission_handler_apple/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" - sqflite: - :path: ".symlinks/plugins/sqflite/darwin" + sqflite_darwin: + :path: ".symlinks/plugins/sqflite_darwin/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: - device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 + device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89 DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 - file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49 + file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_localization: f43b18844a2b3d2c71fd64f04ffd6b1e64dd54d4 - image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 - integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 - launch_review: 75d5a956ba8eaa493e9c9d4bf4c05e505e8d5ed0 - package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 - path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c - permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 + flutter_localization: 72299fb6cb4e51cae587bd953ed0b958040b71e6 + image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e + launch_review_latest: 28e236fc255d91ec1430c39f951c4db1398c79c1 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d SDWebImage: dfe95b2466a9823cf9f0c6d01217c06550d7b29a - shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 - sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d PODFILE CHECKSUM: aa9c826e174e713c4dad1b0d2110be4d87591fc5 COCOAPODS: 1.16.2 - diff --git a/lib/core/providers/state/layers_panel_visibility_state_provider.dart b/lib/core/providers/state/layers_panel_visibility_state_provider.dart new file mode 100644 index 00000000..ae1008c8 --- /dev/null +++ b/lib/core/providers/state/layers_panel_visibility_state_provider.dart @@ -0,0 +1,24 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'layers_panel_visibility_state_provider.g.dart'; + +@riverpod +class LayersPanelVisibilityStateProvider + extends _$LayersPanelVisibilityStateProvider { + void toggleVisibility() { + state = !state; + } + + void show() { + state = true; + } + + void hide() { + state = false; + } + + @override + bool build() { + return false; + } +} diff --git a/lib/core/providers/state/layers_panel_visibility_state_provider.g.dart b/lib/core/providers/state/layers_panel_visibility_state_provider.g.dart new file mode 100644 index 00000000..70ef686e --- /dev/null +++ b/lib/core/providers/state/layers_panel_visibility_state_provider.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'layers_panel_visibility_state_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$layersPanelVisibilityStateProviderHash() => + r'14e8d8ab10b42dfdc185f319871426fd62bfbdbd'; + +/// See also [LayersPanelVisibilityStateProvider]. +@ProviderFor(LayersPanelVisibilityStateProvider) +final layersPanelVisibilityStateProvider = AutoDisposeNotifierProvider< + LayersPanelVisibilityStateProvider, bool>.internal( + LayersPanelVisibilityStateProvider.new, + name: r'layersPanelVisibilityStateProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$layersPanelVisibilityStateProviderHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$LayersPanelVisibilityStateProvider = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/ui/pages/workspace_page/components/bottom_bar/bottom_nav_bar.dart b/lib/ui/pages/workspace_page/components/bottom_bar/bottom_nav_bar.dart index 6f23f496..4711a7ee 100644 --- a/lib/ui/pages/workspace_page/components/bottom_bar/bottom_nav_bar.dart +++ b/lib/ui/pages/workspace_page/components/bottom_bar/bottom_nav_bar.dart @@ -5,6 +5,7 @@ import 'package:colorpicker/colorpicker.dart'; import 'package:paintroid/core/enums/tool_types.dart'; import 'package:paintroid/core/localization/app_localizations.dart'; +import 'package:paintroid/core/providers/state/layers_panel_visibility_state_provider.dart'; import 'package:paintroid/core/providers/state/paint_provider.dart'; import 'package:paintroid/core/providers/state/tool_options_visibility_state_provider.dart'; import 'package:paintroid/core/providers/state/toolbox_state_provider.dart'; @@ -92,6 +93,9 @@ void _onNavigationItemSelected(int index, BuildContext context, WidgetRef ref) { case BottomNavBarItem.COLOR: _showColorPicker(context, ref); break; + case BottomNavBarItem.LAYERS: + _handleLayersPanelVisibility(ref); + break; default: return; } @@ -112,6 +116,10 @@ void _handleToolOptionsVisibility(WidgetRef ref) { ref.read(toolOptionsVisibilityStateProvider.notifier).toggleVisibility(); } +void _handleLayersPanelVisibility(WidgetRef ref) { + ref.read(layersPanelVisibilityStateProvider.notifier).toggleVisibility(); +} + void _showColorPicker(BuildContext context, WidgetRef ref) { showModalBottomSheet( context: context, diff --git a/lib/ui/pages/workspace_page/components/layers_panel/layers_panel.dart b/lib/ui/pages/workspace_page/components/layers_panel/layers_panel.dart new file mode 100644 index 00000000..d79013ca --- /dev/null +++ b/lib/ui/pages/workspace_page/components/layers_panel/layers_panel.dart @@ -0,0 +1,360 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:paintroid/core/providers/state/layers_panel_visibility_state_provider.dart'; + +class LayerData { + final String id; + final double opacity; + final bool isVisible; + + LayerData({ + required this.id, + this.opacity = 100.0, + this.isVisible = true, + }); + + LayerData copyWith({ + String? id, + double? opacity, + bool? isVisible, + }) { + return LayerData( + id: id ?? this.id, + opacity: opacity ?? this.opacity, + isVisible: isVisible ?? this.isVisible, + ); + } +} + +class LayersPanel extends ConsumerStatefulWidget { + const LayersPanel({super.key}); + + static const double panelWidth = 260.0; + + @override + ConsumerState createState() => _LayersPanelState(); +} + +class _LayersPanelState extends ConsumerState { + List layers = [ + LayerData(id: 'Layer 1', opacity: 100), + ]; + + int selectedIndex = 0; + + @override + Widget build(BuildContext context) { + final isVisible = ref.watch(layersPanelVisibilityStateProvider); + final mediaQuery = MediaQuery.of(context); + final screenHeight = mediaQuery.size.height; + final topPadding = mediaQuery.padding.top; + final bottomPadding = mediaQuery.padding.bottom; + + // Account for app bar (~56) and bottom nav bar (~64) plus some spacing + const appBarHeight = 56.0; + const bottomNavBarHeight = 64.0; + const verticalSpacing = 16.0; + + final maxPanelHeight = screenHeight - + appBarHeight - + bottomNavBarHeight - + topPadding - + bottomPadding - + (verticalSpacing * 2); + + return Align( + alignment: Alignment.centerRight, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + transform: Matrix4.translationValues( + isVisible ? 0 : LayersPanel.panelWidth, + 0, + 0, + ), + child: Material( + elevation: 8, + child: Container( + width: LayersPanel.panelWidth, + constraints: BoxConstraints( + maxHeight: maxPanelHeight, + ), + color: const Color(0xFF4A7C8C), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildHeader(), + Flexible( + child: SingleChildScrollView( + child: _buildLayersList(), + ), + ), + _buildFooter(), + ], + ), + ), + ), + ), + ); + } + + Widget _buildHeader() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: const Icon(Icons.visibility, color: Colors.white70), + onPressed: () {}, + tooltip: 'Toggle all layers visibility', + ), + IconButton( + icon: const Icon(Icons.water_drop, color: Colors.white70), + onPressed: () {}, + tooltip: 'Opacity', + ), + IconButton( + icon: const Icon(Icons.add, color: Colors.white), + onPressed: _addNewLayer, + tooltip: 'Add new layer', + ), + IconButton( + icon: const Icon(Icons.delete, color: Colors.white70), + onPressed: _deleteSelectedLayer, + tooltip: 'Delete layer', + ), + ], + ), + ); + } + + Widget _buildLayersList() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + child: ReorderableListView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + onReorder: (oldIndex, newIndex) { + setState(() { + if (newIndex > oldIndex) { + newIndex -= 1; + } + final item = layers.removeAt(oldIndex); + layers.insert(newIndex, item); + + // Update selected index + if (selectedIndex == oldIndex) { + selectedIndex = newIndex; + } else if (oldIndex < selectedIndex && + newIndex >= selectedIndex) { + selectedIndex -= 1; + } else if (oldIndex > selectedIndex && + newIndex <= selectedIndex) { + selectedIndex += 1; + } + }); + }, + children: [ + for (int index = 0; index < layers.length; index++) + Container( + key: ValueKey(layers[index].id), + child: _buildLayerItem(index), + ), + ], + ), + ), + ], + ); + } + + Widget _buildLayerItem(int index) { + final layer = layers[index]; + final isSelected = index == selectedIndex; + + return InkWell( + onTap: () { + setState(() { + selectedIndex = index; + }); + }, + child: Container( + margin: const EdgeInsets.only(bottom: 4), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFF3D6B7A), + border: isSelected + ? Border.all(color: Colors.white.withOpacity(0.3), width: 1) + : null, + ), + child: Row( + children: [ + // Visibility checkbox + SizedBox( + width: 20, + height: 20, + child: Checkbox( + value: layer.isVisible, + onChanged: (value) { + setState(() { + layers[index] = layer.copyWith(isVisible: value ?? true); + }); + }, + activeColor: Colors.white, + checkColor: const Color(0xFF3D6B7A), + side: const BorderSide(color: Colors.white70, width: 1.5), + ), + ), + const SizedBox(width: 12), + // Drag handle lines icon (custom three horizontal lines) + ReorderableDragStartListener( + index: index, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 20, + height: 2, + color: Colors.white54, + ), + const SizedBox(height: 3), + Container( + width: 20, + height: 2, + color: Colors.white54, + ), + const SizedBox(height: 3), + Container( + width: 20, + height: 2, + color: Colors.white54, + ), + ], + ), + ), + const SizedBox(width: 16), + // Vertical opacity slider with label + SizedBox( + height: 110, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: RotatedBox( + quarterTurns: 3, + child: SliderTheme( + data: SliderThemeData( + trackHeight: 3, + activeTrackColor: Colors.white, + inactiveTrackColor: Colors.white30, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 8, + ), + overlayShape: const RoundSliderOverlayShape( + overlayRadius: 14, + ), + thumbColor: Colors.white, + ), + child: Slider( + value: layer.opacity, + min: 0, + max: 100, + onChanged: (value) { + setState(() { + layers[index] = layer.copyWith(opacity: value); + }); + }, + ), + ), + ), + ), + const SizedBox(height: 6), + Text( + '${layer.opacity.round()}', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + const Spacer(), + // Layer thumbnail + Container( + width: 90, + height: 90, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.white30, width: 1), + ), + child: CustomPaint( + painter: CheckerboardPainter(), + child: const Center( + child: Icon( + Icons.image_outlined, + color: Colors.grey, + size: 36, + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildFooter() { + return const SizedBox.shrink(); + } + + void _addNewLayer() { + setState(() { + layers.add(LayerData( + id: 'Layer ${layers.length + 1}', + opacity: 100, + )); + }); + } + + void _deleteSelectedLayer() { + if (layers.length > 1 && + selectedIndex >= 0 && + selectedIndex < layers.length) { + setState(() { + layers.removeAt(selectedIndex); + if (selectedIndex >= layers.length) { + selectedIndex = layers.length - 1; + } + }); + } + } +} + +// Custom painter for checkerboard pattern (transparency indicator) +class CheckerboardPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + const squareSize = 8.0; + final paint = Paint()..color = Colors.grey.shade300; + + for (double i = 0; i < size.width; i += squareSize) { + for (double j = 0; j < size.height; j += squareSize) { + if ((i ~/ squareSize + j ~/ squareSize) % 2 == 0) { + canvas.drawRect( + Rect.fromLTWH(i, j, squareSize, squareSize), + paint, + ); + } + } + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/lib/ui/pages/workspace_page/workspace_page.dart b/lib/ui/pages/workspace_page/workspace_page.dart index a52c364f..b6164435 100644 --- a/lib/ui/pages/workspace_page/workspace_page.dart +++ b/lib/ui/pages/workspace_page/workspace_page.dart @@ -7,6 +7,7 @@ import 'package:paintroid/ui/pages/workspace_page/components/bottom_bar/bottom_n import 'package:paintroid/ui/pages/workspace_page/components/bottom_bar/tool_options/tool_options.dart'; import 'package:paintroid/ui/pages/workspace_page/components/drawing_surface/drawing_canvas.dart'; import 'package:paintroid/ui/pages/workspace_page/components/drawing_surface/exit_fullscreen_button.dart'; +import 'package:paintroid/ui/pages/workspace_page/components/layers_panel/layers_panel.dart'; import 'package:paintroid/ui/pages/workspace_page/components/top_bar/top_app_bar.dart'; import 'package:paintroid/ui/shared/dialogs/discard_changes_dialog.dart'; import 'package:toast/toast.dart'; @@ -76,6 +77,7 @@ class _WorkspaceScreenState extends ConsumerState { ) else const ToolOptions(), + if (!isFullscreen) const LayersPanel(), ], ), bottomNavigationBar: isFullscreen ? null : const BottomNavBar(), From 8d9fee2fffb442185991852fa39d2b71b573a452 Mon Sep 17 00:00:00 2001 From: Lenkomotive Date: Thu, 30 Oct 2025 15:52:40 +0100 Subject: [PATCH 7/8] PAINTROID-454: Flutter: Add Layers --- .../workspace_page/components/layers_panel/layers_panel.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/ui/pages/workspace_page/components/layers_panel/layers_panel.dart b/lib/ui/pages/workspace_page/components/layers_panel/layers_panel.dart index d79013ca..f74e046e 100644 --- a/lib/ui/pages/workspace_page/components/layers_panel/layers_panel.dart +++ b/lib/ui/pages/workspace_page/components/layers_panel/layers_panel.dart @@ -50,7 +50,6 @@ class _LayersPanelState extends ConsumerState { final topPadding = mediaQuery.padding.top; final bottomPadding = mediaQuery.padding.bottom; - // Account for app bar (~56) and bottom nav bar (~64) plus some spacing const appBarHeight = 56.0; const bottomNavBarHeight = 64.0; const verticalSpacing = 16.0; From d8abc37ffb1de26487480f71641d54fa1bd4d809 Mon Sep 17 00:00:00 2001 From: Lenkomotive <90652966+Lenkomotive@users.noreply.github.com> Date: Thu, 30 Oct 2025 15:56:09 +0100 Subject: [PATCH 8/8] Delete lib/core/providers/object/search_filter_sort_provider.dart --- .../providers/object/search_filter_sort_provider.dart | 8 -------- 1 file changed, 8 deletions(-) delete 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 deleted file mode 100644 index 8d12e6b3..00000000 --- a/lib/core/providers/object/search_filter_sort_provider.dart +++ /dev/null @@ -1,8 +0,0 @@ -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, -);