From 03949926673a7300c45ebe2b7b8756d8ea7b5339 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Tue, 12 Nov 2024 22:35:02 +0530 Subject: [PATCH 01/14] experiment apps page ui --- app/lib/pages/apps/page.dart | 164 ++++++++++++++++++++++++++++++++++- 1 file changed, 162 insertions(+), 2 deletions(-) diff --git a/app/lib/pages/apps/page.dart b/app/lib/pages/apps/page.dart index 75cde5161..6856a6ea6 100644 --- a/app/lib/pages/apps/page.dart +++ b/app/lib/pages/apps/page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/backend/schema/app.dart'; import 'package:friend_private/pages/apps/add_app.dart'; import 'package:friend_private/pages/apps/list_item.dart'; @@ -13,14 +14,173 @@ import 'package:provider/provider.dart'; class AppsPage extends StatefulWidget { final bool filterChatOnly; - const AppsPage({super.key, this.filterChatOnly = false}); @override State createState() => _AppsPageState(); } -class _AppsPageState extends State with AutomaticKeepAliveClientMixin { +class _AppsPageState extends State { + @override + void initState() { + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().getCategories(); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.primary, + appBar: null, + body: DefaultTabController( + length: 2, + initialIndex: 0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + TabBar( + indicatorSize: TabBarIndicatorSize.label, + isScrollable: true, + padding: EdgeInsets.zero, + indicatorPadding: EdgeInsets.zero, + labelStyle: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 18), + indicatorColor: Colors.white, + tabs: const [ + Tab(text: 'Explore & Install'), + Tab(text: 'Manage & Create'), + ], + ), + const Spacer(), + const Icon( + Icons.search, + color: Colors.white, + ), + const SizedBox( + width: 12, + ), + ], + ), + Expanded( + child: TabBarView( + children: [ + // For You + CustomScrollView( + slivers: [ + const SliverToBoxAdapter(child: SizedBox(height: 18)), + SliverToBoxAdapter( + child: SizedBox( + height: 40, + child: Selector>( + selector: (ctx, provider) => provider.categories, + builder: (ctx, categories, child) { + return ListView.separated( + scrollDirection: Axis.horizontal, + itemBuilder: (ctx, idx) { + if (idx == 0) { + return Padding( + padding: const EdgeInsets.only(left: 8.0), + child: ChoiceChip( + label: const Row( + children: [ + Icon( + Icons.filter_list_alt, + size: 15, + ), + SizedBox(width: 4), + Text('Filter'), + ], + ), + selected: false, + showCheckmark: true, + backgroundColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + onSelected: (bool selected) {}, + ), + ); + } + return ChoiceChip( + label: Text(categories[idx].title), + selected: false, + showCheckmark: true, + backgroundColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + onSelected: (bool selected) {}, + ); + }, + separatorBuilder: (ctx, idx) { + return const SizedBox( + width: 10, + ); + }, + itemCount: categories.length + 1); + }, + ), + ), + ), + Selector>( + selector: (context, provider) => provider.apps.where((p) => p.worksExternally()).toList(), + builder: (context, memoryIntegrationApps, child) { + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return AppListItem( + app: memoryIntegrationApps[index], + index: index, + ); + }, + childCount: memoryIntegrationApps.length, + ), + ); + }, + ), + ], + ), + // Memories + Selector>( + selector: (context, provider) => provider.apps.where((p) => p.worksWithMemories()).toList(), + builder: (context, memoryPromptApps, child) { + return ListView.separated( + shrinkWrap: true, + itemBuilder: (ctx, index) { + return AppListItem( + app: memoryPromptApps[index], + index: index, + ); + }, + separatorBuilder: (ctx, index) { + return const SizedBox(); + }, + itemCount: memoryPromptApps.length, + ); + }, + ), + ], + )), + ], + ), + ), + ); + } +} + +class AppsPage2 extends StatefulWidget { + final bool filterChatOnly; + + const AppsPage2({super.key, this.filterChatOnly = false}); + + @override + State createState() => _AppsPage2State(); +} + +class _AppsPage2State extends State with AutomaticKeepAliveClientMixin { @override void initState() { WidgetsBinding.instance.addPostFrameCallback((_) { From dba16b46913d937fb01705e64dbc0a699f34fc1e Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Wed, 13 Nov 2024 18:06:31 +0530 Subject: [PATCH 02/14] add filtering --- app/lib/pages/apps/widgets/filter_sheet.dart | 222 +++++++++++++++++++ app/lib/providers/app_provider.dart | 67 ++++++ 2 files changed, 289 insertions(+) create mode 100644 app/lib/pages/apps/widgets/filter_sheet.dart diff --git a/app/lib/pages/apps/widgets/filter_sheet.dart b/app/lib/pages/apps/widgets/filter_sheet.dart new file mode 100644 index 000000000..79a00099a --- /dev/null +++ b/app/lib/pages/apps/widgets/filter_sheet.dart @@ -0,0 +1,222 @@ +import 'package:flutter/material.dart'; +import 'package:friend_private/providers/app_provider.dart'; +import 'package:provider/provider.dart'; + +class FilterBottomSheet extends StatelessWidget { + const FilterBottomSheet({super.key}); + + @override + Widget build(BuildContext context) { + return DraggableScrollableSheet( + expand: false, + maxChildSize: 0.8, + initialChildSize: 0.6, + minChildSize: 0.4, + builder: (context, scrollController) { + return Consumer(builder: (context, provider, child) { + return Scaffold( + body: SingleChildScrollView( + controller: scrollController, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Filters', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + provider.filterApps(); + Navigator.of(context).pop(); + }, + ), + ], + ), + const SizedBox(height: 16), + FilterSection( + title: 'Sort', + child: Column( + children: [ + FilterOption( + label: 'A-Z', + onTap: () { + provider.addOrRemoveFilter('A-Z', 'Sort'); + }, + isSelected: provider.isFilterSelected('A-Z', 'Sort'), + ), + FilterOption( + label: 'Z-A', + onTap: () { + provider.addOrRemoveFilter('Z-A', 'Sort'); + }, + isSelected: provider.isFilterSelected('Z-A', 'Sort'), + ), + FilterOption( + label: 'Highest Rating', + onTap: () { + provider.addOrRemoveFilter('Highest Rating', 'Sort'); + }, + isSelected: provider.isFilterSelected('Highest Rating', 'Sort'), + ), + FilterOption( + label: 'Lowest Rating', + onTap: () { + provider.addOrRemoveFilter('Lowest Rating', 'Sort'); + }, + ), + ], + )), + FilterSection( + title: 'Category', + child: Column( + children: provider.categories + .map((category) => FilterOption( + label: category.title, + onTap: () { + provider.addOrRemoveFilter(category.id, 'Category'); + }, + isSelected: provider.isFilterSelected(category.id, 'Category'), + )) + .toList(), + ), + ), + FilterSection( + title: 'Rating', + child: Column( + children: [ + FilterOption( + label: '1+ Star', + onTap: () { + provider.addOrRemoveFilter('1+', 'Rating'); + }, + isSelected: provider.isFilterSelected('1+', 'Rating')), + FilterOption( + label: '2+ Stars', + onTap: () { + provider.addOrRemoveFilter('2+', 'Rating'); + }, + isSelected: provider.isFilterSelected('2+', 'Rating')), + FilterOption( + label: '3+ Stars', + onTap: () { + provider.addOrRemoveFilter('3+', 'Rating'); + }, + isSelected: provider.isFilterSelected('3+', 'Rating')), + FilterOption( + label: '4+ Stars', + onTap: () { + provider.addOrRemoveFilter('4+', 'Rating'); + }, + isSelected: provider.isFilterSelected('4+', 'Rating')), + ], + ), + ), + FilterSection( + title: 'Capabilities', + child: Column( + children: [ + FilterOption( + label: 'Memories', + onTap: () { + provider.addOrRemoveFilter('memories', 'Capabilities'); + }, + isSelected: provider.isFilterSelected('memories', 'Capabilities')), + FilterOption( + label: 'Chat', + onTap: () { + provider.addOrRemoveFilter('chat', 'Capabilities'); + }, + isSelected: provider.isFilterSelected('chat', 'Capabilities')), + FilterOption( + label: 'Integrations', + onTap: () { + provider.addOrRemoveFilter('external_integration', 'Capabilities'); + }, + isSelected: provider.isFilterSelected('external_integration', 'Capabilities')), + ], + ), + ), + const SizedBox(height: 20), + ], + ), + ), + ), + bottomNavigationBar: Padding( + padding: const EdgeInsets.fromLTRB(40, 10, 40, 40), + child: ElevatedButton( + onPressed: () { + provider.clearFilters(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey[300], + padding: const EdgeInsets.symmetric(horizontal: 32), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: const Text('Clear filters'), + ), + ), + ); + }); + }, + ); + } +} + +class FilterSection extends StatelessWidget { + final String title; + final Widget? child; + + const FilterSection({super.key, required this.title, this.child}); + + @override + Widget build(BuildContext context) { + return ExpansionTile( + iconColor: Colors.white, + title: Text( + title, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500, color: Colors.white), + ), + children: [if (child != null) child!], + ); + } +} + +class FilterOption extends StatelessWidget { + final String label; + final Function()? onTap; + final bool isSelected; + + const FilterOption({super.key, required this.label, this.onTap, this.isSelected = false}); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: SizedBox( + height: 22.0, + width: 22.0, + child: Checkbox( + shape: const CircleBorder(), + value: isSelected, + onChanged: (value) { + if (onTap != null) { + onTap!(); + } + }, + ), + ), + title: Text(label), + onTap: onTap, + ); + } +} diff --git a/app/lib/providers/app_provider.dart b/app/lib/providers/app_provider.dart index 5651e2c87..5e8fcadad 100644 --- a/app/lib/providers/app_provider.dart +++ b/app/lib/providers/app_provider.dart @@ -24,6 +24,73 @@ class AppProvider extends BaseProvider { bool isLoading = false; + List categories = []; + Map filters = {}; + List filteredApps = []; + + void addOrRemoveFilter(String filter, String filterGroup) { + if (filters.containsKey(filterGroup)) { + filters[filterGroup] = filter; + } else { + filters.addAll({filterGroup: filter}); + } + notifyListeners(); + } + + bool isFilterSelected(String filter, String filterGroup) { + return filters.containsKey(filterGroup) && filters[filterGroup] == filter; + } + + void clearFilters() { + filters.clear(); + notifyListeners(); + } + + void removeFilter(String filterGroup) { + filters.remove(filterGroup); + notifyListeners(); + } + + bool isFilterActive() { + return filters.isNotEmpty; + } + + void filterApps() { + if (!isFilterActive()) { + filteredApps = apps; + return; + } + filteredApps = apps; + filters.forEach((key, value) { + switch (key) { + case 'Sort': + if (value == 'A-Z') { + filteredApps.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); + } else if (value == 'Z-A') { + filteredApps.sort((a, b) => b.name.toLowerCase().compareTo(a.name.toLowerCase())); + } else if (value == 'Highest Rating') { + filteredApps.sort((a, b) => (b.ratingAvg ?? 0.0).compareTo(a.ratingAvg ?? 0.0)); + } else if (value == 'Lowest Rating') { + filteredApps.sort((a, b) => (a.ratingAvg ?? 0.0).compareTo(b.ratingAvg ?? 0.0)); + } + break; + case 'Category': + filteredApps = filteredApps.where((app) => app.category == value).toList(); + break; + case 'Rating': + value = value.replaceAll('+', ''); + filteredApps = filteredApps.where((app) => (app.ratingAvg ?? 0.0) >= double.parse(value)).toList(); + break; + case 'Capabilities': + filteredApps = filteredApps.where((app) => app.capabilities.contains(value)).toList(); + break; + default: + break; + } + }); + notifyListeners(); + } + void setIsLoading(bool value) { isLoading = value; notifyListeners(); From 7bb64b306943a62be4c3f7902a5c93fa87f1701a Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Wed, 13 Nov 2024 18:06:41 +0530 Subject: [PATCH 03/14] create category card --- app/lib/pages/apps/widgets/category_card.dart | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 app/lib/pages/apps/widgets/category_card.dart diff --git a/app/lib/pages/apps/widgets/category_card.dart b/app/lib/pages/apps/widgets/category_card.dart new file mode 100644 index 000000000..95cc61aab --- /dev/null +++ b/app/lib/pages/apps/widgets/category_card.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:friend_private/backend/schema/app.dart'; +import 'package:friend_private/pages/apps/list_item.dart'; + +class CategoryCard extends StatelessWidget { + final String title; + final List apps; + final double? height; + const CategoryCard({super.key, required this.title, required this.apps, this.height}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.only(top: 16.0, left: 10, right: 10), + height: height ?? MediaQuery.sizeOf(context).height * 0.4, + margin: const EdgeInsets.only(left: 6.0, right: 6.0, top: 12, bottom: 14), + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(16.0), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text(title, style: const TextStyle(color: Colors.white, fontSize: 18)), + ), + const SizedBox(height: 16), + Expanded( + child: GridView.builder( + scrollDirection: Axis.horizontal, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + childAspectRatio: 0.3, + crossAxisSpacing: 6, + mainAxisSpacing: 6, + ), + itemCount: apps.length, + itemBuilder: (context, index) => AppItemCard( + app: apps[index], + index: index, + ), + ), + ), + ], + ), + ); + } +} From 95f7374bd05e003b023fbfc5a6a5d2e8841fc9a0 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Wed, 13 Nov 2024 18:07:15 +0530 Subject: [PATCH 04/14] improve explore and install page ui --- app/lib/pages/apps/explore_install_page.dart | 194 ++++++++++++++++++ app/lib/pages/apps/list_item.dart | 96 ++++++++- app/lib/pages/apps/page.dart | 113 ++-------- .../apps/providers/add_app_provider.dart | 1 + 4 files changed, 303 insertions(+), 101 deletions(-) create mode 100644 app/lib/pages/apps/explore_install_page.dart diff --git a/app/lib/pages/apps/explore_install_page.dart b/app/lib/pages/apps/explore_install_page.dart new file mode 100644 index 000000000..7cd222f40 --- /dev/null +++ b/app/lib/pages/apps/explore_install_page.dart @@ -0,0 +1,194 @@ +import 'package:flutter/material.dart'; +import 'package:friend_private/backend/schema/app.dart'; +import 'package:friend_private/pages/apps/widgets/category_card.dart'; +import 'package:friend_private/pages/apps/widgets/filter_sheet.dart'; +import 'package:friend_private/pages/apps/list_item.dart'; +import 'package:friend_private/providers/app_provider.dart'; +import 'package:provider/provider.dart'; + +class ExploreInstallPage extends StatelessWidget { + const ExploreInstallPage({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, provider, child) { + return CustomScrollView( + slivers: [ + const SliverToBoxAdapter(child: SizedBox(height: 18)), + SliverToBoxAdapter( + child: SizedBox( + height: 50, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: ChoiceChip( + label: Row( + children: [ + const Icon( + Icons.filter_list_alt, + size: 15, + ), + const SizedBox(width: 4), + const Padding( + padding: EdgeInsets.symmetric(vertical: 6.5), + child: Text( + 'Filter ', + style: TextStyle(fontSize: 16), + ), + ), + provider.isFilterActive() ? Text("(${provider.filters.length})") : const SizedBox.shrink(), + ], + ), + selected: false, + showCheckmark: true, + backgroundColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + onSelected: (bool selected) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (context) => const FilterBottomSheet(), + ).whenComplete(() { + context.read().filterApps(); + }); + }, + ), + ), + const SizedBox( + width: 12, + ), + provider.isFilterActive() + ? Expanded( + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemBuilder: (ctx, idx) { + return ChoiceChip( + labelPadding: const EdgeInsets.only(left: 8), + label: Row( + children: [ + Padding( + padding: EdgeInsets.symmetric(vertical: 6.5), + child: Text( + provider.filters.values.elementAt(idx), + style: const TextStyle(fontSize: 16), + ), + ), + const SizedBox(width: 6), + const Icon( + Icons.close, + size: 15, + ), + ], + ), + selected: false, + showCheckmark: true, + backgroundColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + onSelected: (bool selected) { + provider.removeFilter(provider.filters.keys.elementAt(idx)); + }, + ); + }, + separatorBuilder: (ctx, idx) { + return const SizedBox( + width: 10, + ); + }, + itemCount: provider.filters.length, + )) + : SizedBox( + width: MediaQuery.sizeOf(context).width * 0.72, + height: 40, + child: TextFormField( + onChanged: (value) { + provider.searchQuery = value; + provider.filterApps(); + }, + decoration: InputDecoration( + hintText: 'Search apps', + hintStyle: const TextStyle(color: Colors.white), + filled: true, + fillColor: Colors.grey[800], + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 12), + ), + style: const TextStyle(color: Colors.white), + ), + ) + ], + ), + ), + ), + provider.isFilterActive() + ? const SliverToBoxAdapter(child: SizedBox.shrink()) + : const SliverToBoxAdapter( + child: SizedBox( + height: 10, + )), + !provider.isFilterActive() + ? const SliverToBoxAdapter(child: SizedBox.shrink()) + : Consumer( + builder: (context, provider, child) { + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return AppListItem( + app: provider.filteredApps[index], + index: index, + ); + }, + childCount: provider.filteredApps.length, + ), + ); + }, + ), + !provider.isFilterActive() + ? SliverToBoxAdapter( + child: CategoryCard( + title: 'Popular Apps', + apps: context + .read() + .apps + .where((p) => (p.installs > 50 && (p.ratingAvg ?? 0.0) > 4.0)) + .toList(), + ), + ) + : const SliverToBoxAdapter(child: SizedBox.shrink()), + const SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.only(left: 12.0, top: 18, bottom: 0), + child: Text('All Apps', style: TextStyle(fontSize: 18)), + ), + ), + Selector>( + selector: (context, provider) => provider.apps, + builder: (context, memoryIntegrationApps, child) { + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return AppListItem( + app: memoryIntegrationApps[index], + index: index, + ); + }, + childCount: memoryIntegrationApps.length, + ), + ); + }, + ), + ], + ); + }); + } +} diff --git a/app/lib/pages/apps/list_item.dart b/app/lib/pages/apps/list_item.dart index c9ff57df3..98a76f8e4 100644 --- a/app/lib/pages/apps/list_item.dart +++ b/app/lib/pages/apps/list_item.dart @@ -58,7 +58,7 @@ class AppListItem extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - app.name + (app.private ? ' (private)' : ''), + app.name.decodeString + (app.private ? ' (private)' : ''), maxLines: 1, style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 16), ), @@ -154,3 +154,97 @@ class AppListItem extends StatelessWidget { }); } } + +class AppItemCard extends StatelessWidget { + final App app; + final int index; + + const AppItemCard({super.key, required this.app, required this.index}); + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, provider, child) { + return GestureDetector( + onTap: () async { + MixpanelManager().pageOpened('App Detail'); + await routeToPage(context, AppDetailPage(app: app)); + provider.setApps(); + }, + child: Container( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CachedNetworkImage( + imageUrl: app.getImageUrl(), + imageBuilder: (context, imageProvider) => Container( + width: 52, + height: 52, + decoration: BoxDecoration( + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(8), + image: DecorationImage(image: imageProvider, fit: BoxFit.cover), + ), + ), + placeholder: (context, url) => const SizedBox( + width: 52, + height: 52, + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ), + errorWidget: (context, url, error) => const Icon(Icons.error), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + app.name, + maxLines: 1, + style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 16), + ), + SizedBox(height: app.ratingAvg != null ? 4 : 0), + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + app.category.decodeString, + maxLines: 2, + style: const TextStyle(color: Colors.grey, fontSize: 14), + ), + ), + app.ratingAvg != null || app.installs > 0 + ? Padding( + padding: const EdgeInsets.only(top: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + app.ratingAvg != null + ? Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(app.getRatingAvg()!), + const SizedBox(width: 4), + const Icon(Icons.star, color: Colors.deepPurple, size: 16), + const SizedBox(width: 16), + ], + ) + : const SizedBox(), + ], + ), + ) + : Container(), + ], + ), + ), + const SizedBox(width: 16), + ], + ), + ), + ); + }); + } +} diff --git a/app/lib/pages/apps/page.dart b/app/lib/pages/apps/page.dart index 6856a6ea6..83154fc53 100644 --- a/app/lib/pages/apps/page.dart +++ b/app/lib/pages/apps/page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/backend/schema/app.dart'; import 'package:friend_private/pages/apps/add_app.dart'; +import 'package:friend_private/pages/apps/explore_install_page.dart'; import 'package:friend_private/pages/apps/list_item.dart'; import 'package:friend_private/pages/apps/providers/add_app_provider.dart'; import 'package:friend_private/pages/chat/widgets/animated_mini_banner.dart'; @@ -40,110 +40,23 @@ class _AppsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - TabBar( - indicatorSize: TabBarIndicatorSize.label, - isScrollable: true, - padding: EdgeInsets.zero, - indicatorPadding: EdgeInsets.zero, - labelStyle: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 18), - indicatorColor: Colors.white, - tabs: const [ - Tab(text: 'Explore & Install'), - Tab(text: 'Manage & Create'), - ], - ), - const Spacer(), - const Icon( - Icons.search, - color: Colors.white, - ), - const SizedBox( - width: 12, - ), + TabBar( + indicatorSize: TabBarIndicatorSize.label, + isScrollable: true, + padding: EdgeInsets.zero, + indicatorPadding: EdgeInsets.zero, + labelStyle: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 18), + indicatorColor: Colors.white, + tabs: const [ + Tab(text: 'Explore & Install'), + Tab(text: 'Manage & Create'), ], ), Expanded( child: TabBarView( children: [ - // For You - CustomScrollView( - slivers: [ - const SliverToBoxAdapter(child: SizedBox(height: 18)), - SliverToBoxAdapter( - child: SizedBox( - height: 40, - child: Selector>( - selector: (ctx, provider) => provider.categories, - builder: (ctx, categories, child) { - return ListView.separated( - scrollDirection: Axis.horizontal, - itemBuilder: (ctx, idx) { - if (idx == 0) { - return Padding( - padding: const EdgeInsets.only(left: 8.0), - child: ChoiceChip( - label: const Row( - children: [ - Icon( - Icons.filter_list_alt, - size: 15, - ), - SizedBox(width: 4), - Text('Filter'), - ], - ), - selected: false, - showCheckmark: true, - backgroundColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - onSelected: (bool selected) {}, - ), - ); - } - return ChoiceChip( - label: Text(categories[idx].title), - selected: false, - showCheckmark: true, - backgroundColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - onSelected: (bool selected) {}, - ); - }, - separatorBuilder: (ctx, idx) { - return const SizedBox( - width: 10, - ); - }, - itemCount: categories.length + 1); - }, - ), - ), - ), - Selector>( - selector: (context, provider) => provider.apps.where((p) => p.worksExternally()).toList(), - builder: (context, memoryIntegrationApps, child) { - return SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - return AppListItem( - app: memoryIntegrationApps[index], - index: index, - ); - }, - childCount: memoryIntegrationApps.length, - ), - ); - }, - ), - ], - ), - // Memories + const ExploreInstallPage(), + // Manage & Create Selector>( selector: (context, provider) => provider.apps.where((p) => p.worksWithMemories()).toList(), builder: (context, memoryPromptApps, child) { diff --git a/app/lib/pages/apps/providers/add_app_provider.dart b/app/lib/pages/apps/providers/add_app_provider.dart index f97f69f29..13da872e9 100644 --- a/app/lib/pages/apps/providers/add_app_provider.dart +++ b/app/lib/pages/apps/providers/add_app_provider.dart @@ -136,6 +136,7 @@ class AddAppProvider extends ChangeNotifier { Future getCategories() async { categories = await getAppCategories(); + appProvider!.categories = categories; notifyListeners(); } From df60db7aef5f38fb1906e9accf809d080f38a1fd Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:07:58 +0530 Subject: [PATCH 05/14] hide tabs when searching --- app/lib/providers/home_provider.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/lib/providers/home_provider.dart b/app/lib/providers/home_provider.dart index b44809d72..76f802f98 100644 --- a/app/lib/providers/home_provider.dart +++ b/app/lib/providers/home_provider.dart @@ -8,6 +8,8 @@ class HomeProvider extends ChangeNotifier { int selectedIndex = 0; final FocusNode memoryFieldFocusNode = FocusNode(); final FocusNode chatFieldFocusNode = FocusNode(); + final FocusNode appsSearchFieldFocusNode = FocusNode(); + bool isAppsSearchFieldFocused = false; bool isMemoryFieldFocused = false; bool isChatFieldFocused = false; bool hasSpeakerProfile = true; @@ -54,11 +56,13 @@ class HomeProvider extends ChangeNotifier { HomeProvider() { memoryFieldFocusNode.addListener(_onFocusChange); chatFieldFocusNode.addListener(_onFocusChange); + appsSearchFieldFocusNode.addListener(_onFocusChange); } void _onFocusChange() { isMemoryFieldFocused = memoryFieldFocusNode.hasFocus; isChatFieldFocused = chatFieldFocusNode.hasFocus; + isAppsSearchFieldFocused = appsSearchFieldFocusNode.hasFocus; notifyListeners(); } @@ -107,8 +111,10 @@ class HomeProvider extends ChangeNotifier { void dispose() { memoryFieldFocusNode.removeListener(_onFocusChange); chatFieldFocusNode.removeListener(_onFocusChange); + appsSearchFieldFocusNode.removeListener(_onFocusChange); memoryFieldFocusNode.dispose(); chatFieldFocusNode.dispose(); + appsSearchFieldFocusNode.dispose(); super.dispose(); } } From 7b0e6d405594a2385bb6e7e5688ca2f0603a28f5 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:08:23 +0530 Subject: [PATCH 06/14] add search functionality --- app/lib/pages/apps/explore_install_page.dart | 104 ++++++++++++++----- app/lib/providers/app_provider.dart | 23 ++++ 2 files changed, 100 insertions(+), 27 deletions(-) diff --git a/app/lib/pages/apps/explore_install_page.dart b/app/lib/pages/apps/explore_install_page.dart index 7cd222f40..c50c45e6f 100644 --- a/app/lib/pages/apps/explore_install_page.dart +++ b/app/lib/pages/apps/explore_install_page.dart @@ -4,11 +4,31 @@ import 'package:friend_private/pages/apps/widgets/category_card.dart'; import 'package:friend_private/pages/apps/widgets/filter_sheet.dart'; import 'package:friend_private/pages/apps/list_item.dart'; import 'package:friend_private/providers/app_provider.dart'; +import 'package:friend_private/providers/home_provider.dart'; import 'package:provider/provider.dart'; -class ExploreInstallPage extends StatelessWidget { +class ExploreInstallPage extends StatefulWidget { const ExploreInstallPage({super.key}); + @override + State createState() => _ExploreInstallPageState(); +} + +class _ExploreInstallPageState extends State { + late TextEditingController searchController; + + @override + void initState() { + searchController = TextEditingController(); + super.initState(); + } + + @override + void dispose() { + searchController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Consumer(builder: (context, provider, child) { @@ -73,7 +93,7 @@ class ExploreInstallPage extends StatelessWidget { label: Row( children: [ Padding( - padding: EdgeInsets.symmetric(vertical: 6.5), + padding: const EdgeInsets.symmetric(vertical: 6.5), child: Text( provider.filters.values.elementAt(idx), style: const TextStyle(fontSize: 16), @@ -108,9 +128,10 @@ class ExploreInstallPage extends StatelessWidget { width: MediaQuery.sizeOf(context).width * 0.72, height: 40, child: TextFormField( + controller: searchController, + focusNode: context.read().appsSearchFieldFocusNode, onChanged: (value) { - provider.searchQuery = value; - provider.filterApps(); + provider.searchApps(value); }, decoration: InputDecoration( hintText: 'Search apps', @@ -121,6 +142,19 @@ class ExploreInstallPage extends StatelessWidget { borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none, ), + suffixIcon: provider.isSearchActive() + ? GestureDetector( + onTap: () { + context.read().appsSearchFieldFocusNode.unfocus(); + provider.searchApps(''); + searchController.clear(); + }, + child: const Icon( + Icons.close, + color: Colors.white, + ), + ) + : null, contentPadding: const EdgeInsets.symmetric(horizontal: 12), ), style: const TextStyle(color: Colors.white), @@ -130,16 +164,28 @@ class ExploreInstallPage extends StatelessWidget { ), ), ), - provider.isFilterActive() + provider.isFilterActive() || provider.isSearchActive() ? const SliverToBoxAdapter(child: SizedBox.shrink()) : const SliverToBoxAdapter( child: SizedBox( height: 10, )), - !provider.isFilterActive() + !provider.isFilterActive() && !provider.isSearchActive() ? const SliverToBoxAdapter(child: SizedBox.shrink()) : Consumer( builder: (context, provider, child) { + if (provider.filteredApps.isEmpty) { + return SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.only(top: MediaQuery.sizeOf(context).height * 0.28), + child: const Text( + 'No apps found', + style: TextStyle(fontSize: 18), + textAlign: TextAlign.center, + ), + ), + ); + } return SliverList( delegate: SliverChildBuilderDelegate( (context, index) { @@ -153,7 +199,7 @@ class ExploreInstallPage extends StatelessWidget { ); }, ), - !provider.isFilterActive() + !provider.isFilterActive() && !provider.isSearchActive() ? SliverToBoxAdapter( child: CategoryCard( title: 'Popular Apps', @@ -165,28 +211,32 @@ class ExploreInstallPage extends StatelessWidget { ), ) : const SliverToBoxAdapter(child: SizedBox.shrink()), - const SliverToBoxAdapter( - child: Padding( - padding: EdgeInsets.only(left: 12.0, top: 18, bottom: 0), - child: Text('All Apps', style: TextStyle(fontSize: 18)), - ), - ), - Selector>( - selector: (context, provider) => provider.apps, - builder: (context, memoryIntegrationApps, child) { - return SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - return AppListItem( - app: memoryIntegrationApps[index], - index: index, + !provider.isFilterActive() && !provider.isSearchActive() + ? const SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.only(left: 12.0, top: 18, bottom: 0), + child: Text('All Apps', style: TextStyle(fontSize: 18)), + ), + ) + : const SliverToBoxAdapter(child: SizedBox.shrink()), + !provider.isFilterActive() && !provider.isSearchActive() + ? Selector>( + selector: (context, provider) => provider.apps, + builder: (context, memoryIntegrationApps, child) { + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return AppListItem( + app: memoryIntegrationApps[index], + index: index, + ); + }, + childCount: memoryIntegrationApps.length, + ), ); }, - childCount: memoryIntegrationApps.length, - ), - ); - }, - ), + ) + : const SliverToBoxAdapter(child: SizedBox.shrink()), ], ); }); diff --git a/app/lib/providers/app_provider.dart b/app/lib/providers/app_provider.dart index 5e8fcadad..856ade524 100644 --- a/app/lib/providers/app_provider.dart +++ b/app/lib/providers/app_provider.dart @@ -14,6 +14,7 @@ class AppProvider extends BaseProvider { bool filterMemories = true; bool filterExternal = true; String searchQuery = ''; + bool installedAppsOptionSelected = true; List appLoading = []; @@ -28,6 +29,13 @@ class AppProvider extends BaseProvider { Map filters = {}; List filteredApps = []; + get userPrivateApps => apps.where((app) => app.private).toList(); + + void updateInstalledAppsOptionSelected(bool value) { + installedAppsOptionSelected = value; + notifyListeners(); + } + void addOrRemoveFilter(String filter, String filterGroup) { if (filters.containsKey(filterGroup)) { filters[filterGroup] = filter; @@ -55,6 +63,21 @@ class AppProvider extends BaseProvider { return filters.isNotEmpty; } + bool isSearchActive() { + return searchQuery.isNotEmpty; + } + + void searchApps(String query) { + if (query.isEmpty) { + filteredApps = apps; + searchQuery = ''; + } else { + searchQuery = query; + filteredApps = apps.where((app) => app.name.toLowerCase().contains(query.toLowerCase())).toList(); + } + notifyListeners(); + } + void filterApps() { if (!isFilterActive()) { filteredApps = apps; From 4f99400b99d67d152852f7bd2e9a248e3351687f Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:08:47 +0530 Subject: [PATCH 07/14] manage and create page and other changes --- app/lib/pages/apps/manage_create_page.dart | 112 +++++++++++++++++++++ app/lib/pages/apps/page.dart | 25 +---- app/lib/pages/home/page.dart | 4 +- 3 files changed, 119 insertions(+), 22 deletions(-) create mode 100644 app/lib/pages/apps/manage_create_page.dart diff --git a/app/lib/pages/apps/manage_create_page.dart b/app/lib/pages/apps/manage_create_page.dart new file mode 100644 index 000000000..72d1b6604 --- /dev/null +++ b/app/lib/pages/apps/manage_create_page.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:friend_private/backend/schema/app.dart'; +import 'package:friend_private/pages/apps/list_item.dart'; +import 'package:friend_private/providers/app_provider.dart'; +import 'package:provider/provider.dart'; + +class ManageCreatePage extends StatelessWidget { + const ManageCreatePage({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, provider, child) { + return CustomScrollView( + slivers: [ + const SliverToBoxAdapter(child: SizedBox(height: 18)), + SliverToBoxAdapter( + child: Row( + children: [ + const SizedBox(width: 16), + ChoiceChip( + label: const Text('Installed Apps'), + selected: provider.installedAppsOptionSelected, + showCheckmark: true, + backgroundColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + onSelected: (bool selected) { + provider.updateInstalledAppsOptionSelected(true); + }, + ), + const SizedBox(width: 10), + ChoiceChip( + label: const Text('My Apps'), + selected: !provider.installedAppsOptionSelected, + showCheckmark: true, + backgroundColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + onSelected: (bool selected) { + provider.updateInstalledAppsOptionSelected(false); + }, + ), + ], + ), + ), + SliverToBoxAdapter( + child: AnimatedSwitcher( + transitionBuilder: (child, animation) => FadeTransition( + opacity: animation, + child: child, + ), + duration: const Duration(milliseconds: 500), + child: provider.installedAppsOptionSelected + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Text('Apps (${provider.apps.where((a) => a.enabled).length})', + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w400)), + ), + Selector>( + selector: (context, provider) => provider.apps.where((p) => p.enabled).toList(), + builder: (context, memoryPromptApps, child) { + return ListView.builder( + itemCount: memoryPromptApps.length, + shrinkWrap: true, + itemBuilder: (context, index) { + return AppListItem( + app: memoryPromptApps[index], + index: index, + ); + }); + }, + ), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Text('Private Apps (${provider.userPrivateApps.length})', + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w400)), + ), + Selector>( + selector: (context, provider) => provider.apps.where((p) => p.enabled).toList(), + builder: (context, memoryPromptApps, child) { + return ListView.builder( + itemCount: memoryPromptApps.length, + shrinkWrap: true, + itemBuilder: (context, index) { + return AppListItem( + app: memoryPromptApps[index], + index: index, + ); + }); + }, + ), + ], + ), + ), + ), + ], + ); + }); + } +} diff --git a/app/lib/pages/apps/page.dart b/app/lib/pages/apps/page.dart index 83154fc53..43dd321e7 100644 --- a/app/lib/pages/apps/page.dart +++ b/app/lib/pages/apps/page.dart @@ -3,6 +3,7 @@ import 'package:friend_private/backend/schema/app.dart'; import 'package:friend_private/pages/apps/add_app.dart'; import 'package:friend_private/pages/apps/explore_install_page.dart'; import 'package:friend_private/pages/apps/list_item.dart'; +import 'package:friend_private/pages/apps/manage_create_page.dart'; import 'package:friend_private/pages/apps/providers/add_app_provider.dart'; import 'package:friend_private/pages/chat/widgets/animated_mini_banner.dart'; import 'package:friend_private/providers/connectivity_provider.dart'; @@ -52,29 +53,11 @@ class _AppsPageState extends State { Tab(text: 'Manage & Create'), ], ), - Expanded( + const Expanded( child: TabBarView( children: [ - const ExploreInstallPage(), - // Manage & Create - Selector>( - selector: (context, provider) => provider.apps.where((p) => p.worksWithMemories()).toList(), - builder: (context, memoryPromptApps, child) { - return ListView.separated( - shrinkWrap: true, - itemBuilder: (ctx, index) { - return AppListItem( - app: memoryPromptApps[index], - index: index, - ); - }, - separatorBuilder: (ctx, index) { - return const SizedBox(); - }, - itemCount: memoryPromptApps.length, - ); - }, - ), + ExploreInstallPage(), + ManageCreatePage(), ], )), ], diff --git a/app/lib/pages/home/page.dart b/app/lib/pages/home/page.dart index fae6fae29..9ea278087 100644 --- a/app/lib/pages/home/page.dart +++ b/app/lib/pages/home/page.dart @@ -277,7 +277,9 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker ), Consumer( builder: (context, home, child) { - if (home.chatFieldFocusNode.hasFocus || home.memoryFieldFocusNode.hasFocus) { + if (home.chatFieldFocusNode.hasFocus || + home.memoryFieldFocusNode.hasFocus || + home.appsSearchFieldFocusNode.hasFocus) { return const SizedBox.shrink(); } else { return Align( From 839945e70aded3379d4c9cb01ac7d7fa1cb63968 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Thu, 14 Nov 2024 19:30:02 +0530 Subject: [PATCH 08/14] improve filters --- app/lib/providers/app_provider.dart | 38 ++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/app/lib/providers/app_provider.dart b/app/lib/providers/app_provider.dart index 856ade524..b50e4ccc1 100644 --- a/app/lib/providers/app_provider.dart +++ b/app/lib/providers/app_provider.dart @@ -26,10 +26,14 @@ class AppProvider extends BaseProvider { bool isLoading = false; List categories = []; - Map filters = {}; + List capabilities = []; + Map filters = {}; List filteredApps = []; - get userPrivateApps => apps.where((app) => app.private).toList(); + List get userPrivateApps => apps.where((app) => app.private).toList(); + + List get userPublicApps => + apps.where((app) => (!app.private && app.uid == SharedPreferencesUtil().uid)).toList(); void updateInstalledAppsOptionSelected(bool value) { installedAppsOptionSelected = value; @@ -45,10 +49,36 @@ class AppProvider extends BaseProvider { notifyListeners(); } + void addOrRemoveCategoryFilter(Category category) { + if (filters.containsKey('Category')) { + filters['Category'] = category; + } else { + filters.addAll({'Category': category}); + } + notifyListeners(); + } + + void addOrRemoveCapabilityFilter(AppCapability capability) { + if (filters.containsKey('Capabilities')) { + filters['Capabilities'] = capability; + } else { + filters.addAll({'Capabilities': capability}); + } + notifyListeners(); + } + bool isFilterSelected(String filter, String filterGroup) { return filters.containsKey(filterGroup) && filters[filterGroup] == filter; } + bool isCategoryFilterSelected(Category category) { + return filters.containsKey('Category') && filters['Category'] == category; + } + + bool isCapabilityFilterSelected(AppCapability capability) { + return filters.containsKey('Capabilities') && filters['Capabilities'] == capability; + } + void clearFilters() { filters.clear(); notifyListeners(); @@ -98,14 +128,14 @@ class AppProvider extends BaseProvider { } break; case 'Category': - filteredApps = filteredApps.where((app) => app.category == value).toList(); + filteredApps = filteredApps.where((app) => app.category == (value as Category).id).toList(); break; case 'Rating': value = value.replaceAll('+', ''); filteredApps = filteredApps.where((app) => (app.ratingAvg ?? 0.0) >= double.parse(value)).toList(); break; case 'Capabilities': - filteredApps = filteredApps.where((app) => app.capabilities.contains(value)).toList(); + filteredApps = filteredApps.where((app) => app.capabilities.contains((value as AppCapability).id)).toList(); break; default: break; From 8069efcfde6df35ea87f58627c571c66dbfce310 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Thu, 14 Nov 2024 19:30:48 +0530 Subject: [PATCH 09/14] filter and other ui improvements and cleaning --- app/lib/pages/apps/app_detail.dart | 2 +- app/lib/pages/apps/explore_install_page.dart | 17 +- app/lib/pages/apps/list_item.dart | 181 +++++------------- .../apps/providers/add_app_provider.dart | 1 + .../pages/apps/widgets/app_section_card.dart | 150 +++++++++++++++ app/lib/pages/apps/widgets/category_card.dart | 50 ----- app/lib/pages/apps/widgets/filter_sheet.dart | 36 ++-- .../pages/apps/widgets/info_card_widget.dart | 2 +- 8 files changed, 232 insertions(+), 207 deletions(-) create mode 100644 app/lib/pages/apps/widgets/app_section_card.dart delete mode 100644 app/lib/pages/apps/widgets/category_card.dart diff --git a/app/lib/pages/apps/app_detail.dart b/app/lib/pages/apps/app_detail.dart index dcc8d64de..7d3d663c0 100644 --- a/app/lib/pages/apps/app_detail.dart +++ b/app/lib/pages/apps/app_detail.dart @@ -444,7 +444,7 @@ class _AppDetailPageState extends State { child: Container( width: double.infinity, padding: const EdgeInsets.all(16.0), - margin: const EdgeInsets.only(left: 6.0, right: 6.0, top: 12, bottom: 6), + margin: const EdgeInsets.only(left: 8.0, right: 8.0, top: 12, bottom: 6), decoration: BoxDecoration( color: Colors.grey.shade900, borderRadius: BorderRadius.circular(16.0), diff --git a/app/lib/pages/apps/explore_install_page.dart b/app/lib/pages/apps/explore_install_page.dart index c50c45e6f..16cd7496d 100644 --- a/app/lib/pages/apps/explore_install_page.dart +++ b/app/lib/pages/apps/explore_install_page.dart @@ -1,12 +1,23 @@ import 'package:flutter/material.dart'; import 'package:friend_private/backend/schema/app.dart'; -import 'package:friend_private/pages/apps/widgets/category_card.dart'; +import 'package:friend_private/pages/apps/widgets/app_section_card.dart'; import 'package:friend_private/pages/apps/widgets/filter_sheet.dart'; import 'package:friend_private/pages/apps/list_item.dart'; import 'package:friend_private/providers/app_provider.dart'; import 'package:friend_private/providers/home_provider.dart'; import 'package:provider/provider.dart'; +String filterValueToString(dynamic value) { + if (value.runtimeType == String) { + return value; + } else if (value.runtimeType == Category) { + return (value as Category).title; + } else if (value.runtimeType == AppCapability) { + return (value as AppCapability).title; + } + return ''; +} + class ExploreInstallPage extends StatefulWidget { const ExploreInstallPage({super.key}); @@ -95,7 +106,7 @@ class _ExploreInstallPageState extends State { Padding( padding: const EdgeInsets.symmetric(vertical: 6.5), child: Text( - provider.filters.values.elementAt(idx), + filterValueToString(provider.filters.values.elementAt(idx)), style: const TextStyle(fontSize: 16), ), ), @@ -201,7 +212,7 @@ class _ExploreInstallPageState extends State { ), !provider.isFilterActive() && !provider.isSearchActive() ? SliverToBoxAdapter( - child: CategoryCard( + child: AppSectionCard( title: 'Popular Apps', apps: context .read() diff --git a/app/lib/pages/apps/list_item.dart b/app/lib/pages/apps/list_item.dart index 98a76f8e4..07fda12b6 100644 --- a/app/lib/pages/apps/list_item.dart +++ b/app/lib/pages/apps/list_item.dart @@ -58,7 +58,7 @@ class AppListItem extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - app.name.decodeString + (app.private ? ' (private)' : ''), + app.name.decodeString, maxLines: 1, style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 16), ), @@ -71,40 +71,57 @@ class AppListItem extends StatelessWidget { style: const TextStyle(color: Colors.grey, fontSize: 14), ), ), - app.ratingAvg != null || app.installs > 0 - ? Padding( - padding: const EdgeInsets.only(top: 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - app.ratingAvg != null - ? Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(app.getRatingAvg()!), - const SizedBox(width: 4), - const Icon(Icons.star, color: Colors.deepPurple, size: 16), - const SizedBox(width: 4), - Text('(${app.ratingCount})'), - const SizedBox(width: 16), - ], - ) - : const SizedBox(), - app.installs > 0 - ? Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(Icons.download_rounded, size: 16, color: Colors.grey.shade300), - const SizedBox(width: 4), - Text('${app.installs}'), - ], - ) - : Container(), - ], - ), - ) - : Container(), + Row( + children: [ + app.ratingAvg != null || app.installs > 0 + ? Padding( + padding: const EdgeInsets.only(top: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + app.ratingAvg != null + ? Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(app.getRatingAvg()!), + const SizedBox(width: 4), + const Icon(Icons.star, color: Colors.deepPurple, size: 16), + const SizedBox(width: 4), + Text('(${app.ratingCount})'), + const SizedBox(width: 16), + ], + ) + : const SizedBox(), + app.installs > 0 + ? Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(Icons.download_rounded, size: 16, color: Colors.grey.shade300), + const SizedBox(width: 4), + Text('${app.installs}'), + ], + ) + : Container(), + ], + ), + ) + : Container(), + (app.ratingAvg != null || app.installs > 0) ? const SizedBox(width: 16) : const SizedBox(), + app.private + ? const Padding( + padding: EdgeInsets.only(top: 8), + child: Row( + children: [ + Icon(Icons.lock, color: Colors.grey, size: 16), + SizedBox(width: 4), + Text('Private', style: TextStyle(color: Colors.grey, fontSize: 14)), + ], + ), + ) + : const SizedBox(), + ], + ), ], ), ), @@ -154,97 +171,3 @@ class AppListItem extends StatelessWidget { }); } } - -class AppItemCard extends StatelessWidget { - final App app; - final int index; - - const AppItemCard({super.key, required this.app, required this.index}); - - @override - Widget build(BuildContext context) { - return Consumer(builder: (context, provider, child) { - return GestureDetector( - onTap: () async { - MixpanelManager().pageOpened('App Detail'); - await routeToPage(context, AppDetailPage(app: app)); - provider.setApps(); - }, - child: Container( - padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CachedNetworkImage( - imageUrl: app.getImageUrl(), - imageBuilder: (context, imageProvider) => Container( - width: 52, - height: 52, - decoration: BoxDecoration( - shape: BoxShape.rectangle, - borderRadius: BorderRadius.circular(8), - image: DecorationImage(image: imageProvider, fit: BoxFit.cover), - ), - ), - placeholder: (context, url) => const SizedBox( - width: 52, - height: 52, - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ), - errorWidget: (context, url, error) => const Icon(Icons.error), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - app.name, - maxLines: 1, - style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 16), - ), - SizedBox(height: app.ratingAvg != null ? 4 : 0), - Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Text( - app.category.decodeString, - maxLines: 2, - style: const TextStyle(color: Colors.grey, fontSize: 14), - ), - ), - app.ratingAvg != null || app.installs > 0 - ? Padding( - padding: const EdgeInsets.only(top: 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - app.ratingAvg != null - ? Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(app.getRatingAvg()!), - const SizedBox(width: 4), - const Icon(Icons.star, color: Colors.deepPurple, size: 16), - const SizedBox(width: 16), - ], - ) - : const SizedBox(), - ], - ), - ) - : Container(), - ], - ), - ), - const SizedBox(width: 16), - ], - ), - ), - ); - }); - } -} diff --git a/app/lib/pages/apps/providers/add_app_provider.dart b/app/lib/pages/apps/providers/add_app_provider.dart index 13da872e9..0b76b1591 100644 --- a/app/lib/pages/apps/providers/add_app_provider.dart +++ b/app/lib/pages/apps/providers/add_app_provider.dart @@ -142,6 +142,7 @@ class AddAppProvider extends ChangeNotifier { Future getAppCapabilities() async { capabilities = await getAppCapabilitiesServer(); + appProvider!.capabilities = capabilities; notifyListeners(); } diff --git a/app/lib/pages/apps/widgets/app_section_card.dart b/app/lib/pages/apps/widgets/app_section_card.dart new file mode 100644 index 000000000..a272b61fd --- /dev/null +++ b/app/lib/pages/apps/widgets/app_section_card.dart @@ -0,0 +1,150 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:friend_private/backend/schema/app.dart'; +import 'package:friend_private/pages/apps/app_detail.dart'; +import 'package:friend_private/providers/app_provider.dart'; +import 'package:friend_private/utils/analytics/mixpanel.dart'; +import 'package:friend_private/utils/other/temp.dart'; +import 'package:friend_private/widgets/extensions/string.dart'; +import 'package:provider/provider.dart'; + +class AppSectionCard extends StatelessWidget { + final String title; + final List apps; + final double? height; + const AppSectionCard({super.key, required this.title, required this.apps, this.height}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.only(top: 16.0, left: 10, right: 10), + height: height ?? MediaQuery.sizeOf(context).height * 0.4, + margin: const EdgeInsets.only(left: 6.0, right: 6.0, top: 12, bottom: 14), + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(16.0), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text(title, style: const TextStyle(color: Colors.white, fontSize: 18)), + ), + const SizedBox(height: 16), + Expanded( + child: GridView.builder( + scrollDirection: Axis.horizontal, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + childAspectRatio: 0.3, + crossAxisSpacing: 6, + mainAxisSpacing: 6, + ), + itemCount: apps.length, + itemBuilder: (context, index) => SectionAppItemCard( + app: apps[index], + index: index, + ), + ), + ), + ], + ), + ); + } +} + +class SectionAppItemCard extends StatelessWidget { + final App app; + final int index; + + const SectionAppItemCard({super.key, required this.app, required this.index}); + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, provider, child) { + return GestureDetector( + onTap: () async { + MixpanelManager().pageOpened('App Detail From Popular Apps Section'); + await routeToPage(context, AppDetailPage(app: app)); + provider.setApps(); + }, + child: Container( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CachedNetworkImage( + imageUrl: app.getImageUrl(), + imageBuilder: (context, imageProvider) => Container( + width: 52, + height: 52, + decoration: BoxDecoration( + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(8), + image: DecorationImage(image: imageProvider, fit: BoxFit.cover), + ), + ), + placeholder: (context, url) => const SizedBox( + width: 52, + height: 52, + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ), + errorWidget: (context, url, error) => const Icon(Icons.error), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + app.name, + maxLines: 1, + style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 16), + ), + SizedBox(height: app.ratingAvg != null ? 4 : 0), + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + app.category.decodeString, + maxLines: 2, + style: const TextStyle(color: Colors.grey, fontSize: 14), + ), + ), + app.ratingAvg != null || app.installs > 0 + ? Padding( + padding: const EdgeInsets.only(top: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + app.ratingAvg != null + ? Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(app.getRatingAvg()!), + const SizedBox(width: 4), + const Icon(Icons.star, color: Colors.deepPurple, size: 16), + const SizedBox(width: 16), + ], + ) + : const SizedBox(), + ], + ), + ) + : Container(), + ], + ), + ), + const SizedBox(width: 16), + ], + ), + ), + ); + }); + } +} diff --git a/app/lib/pages/apps/widgets/category_card.dart b/app/lib/pages/apps/widgets/category_card.dart deleted file mode 100644 index 95cc61aab..000000000 --- a/app/lib/pages/apps/widgets/category_card.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:friend_private/backend/schema/app.dart'; -import 'package:friend_private/pages/apps/list_item.dart'; - -class CategoryCard extends StatelessWidget { - final String title; - final List apps; - final double? height; - const CategoryCard({super.key, required this.title, required this.apps, this.height}); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.only(top: 16.0, left: 10, right: 10), - height: height ?? MediaQuery.sizeOf(context).height * 0.4, - margin: const EdgeInsets.only(left: 6.0, right: 6.0, top: 12, bottom: 14), - decoration: BoxDecoration( - color: Colors.grey.shade900, - borderRadius: BorderRadius.circular(16.0), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text(title, style: const TextStyle(color: Colors.white, fontSize: 18)), - ), - const SizedBox(height: 16), - Expanded( - child: GridView.builder( - scrollDirection: Axis.horizontal, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - childAspectRatio: 0.3, - crossAxisSpacing: 6, - mainAxisSpacing: 6, - ), - itemCount: apps.length, - itemBuilder: (context, index) => AppItemCard( - app: apps[index], - index: index, - ), - ), - ), - ], - ), - ); - } -} diff --git a/app/lib/pages/apps/widgets/filter_sheet.dart b/app/lib/pages/apps/widgets/filter_sheet.dart index 79a00099a..0a3c1e281 100644 --- a/app/lib/pages/apps/widgets/filter_sheet.dart +++ b/app/lib/pages/apps/widgets/filter_sheet.dart @@ -72,6 +72,7 @@ class FilterBottomSheet extends StatelessWidget { onTap: () { provider.addOrRemoveFilter('Lowest Rating', 'Sort'); }, + isSelected: provider.isFilterSelected('Lowest Rating', 'Sort'), ), ], )), @@ -82,9 +83,9 @@ class FilterBottomSheet extends StatelessWidget { .map((category) => FilterOption( label: category.title, onTap: () { - provider.addOrRemoveFilter(category.id, 'Category'); + provider.addOrRemoveCategoryFilter(category); }, - isSelected: provider.isFilterSelected(category.id, 'Category'), + isSelected: provider.isCategoryFilterSelected(category), )) .toList(), ), @@ -123,26 +124,15 @@ class FilterBottomSheet extends StatelessWidget { FilterSection( title: 'Capabilities', child: Column( - children: [ - FilterOption( - label: 'Memories', - onTap: () { - provider.addOrRemoveFilter('memories', 'Capabilities'); - }, - isSelected: provider.isFilterSelected('memories', 'Capabilities')), - FilterOption( - label: 'Chat', - onTap: () { - provider.addOrRemoveFilter('chat', 'Capabilities'); - }, - isSelected: provider.isFilterSelected('chat', 'Capabilities')), - FilterOption( - label: 'Integrations', - onTap: () { - provider.addOrRemoveFilter('external_integration', 'Capabilities'); - }, - isSelected: provider.isFilterSelected('external_integration', 'Capabilities')), - ], + children: provider.capabilities + .map((capability) => FilterOption( + label: capability.title, + onTap: () { + provider.addOrRemoveCapabilityFilter(capability); + }, + isSelected: provider.isCapabilityFilterSelected(capability), + )) + .toList(), ), ), const SizedBox(height: 20), @@ -163,7 +153,7 @@ class FilterBottomSheet extends StatelessWidget { borderRadius: BorderRadius.circular(20), ), ), - child: const Text('Clear filters'), + child: const Text('Clear Filters'), ), ), ); diff --git a/app/lib/pages/apps/widgets/info_card_widget.dart b/app/lib/pages/apps/widgets/info_card_widget.dart index 00bbc4788..d737c1d92 100644 --- a/app/lib/pages/apps/widgets/info_card_widget.dart +++ b/app/lib/pages/apps/widgets/info_card_widget.dart @@ -22,7 +22,7 @@ class InfoCardWidget extends StatelessWidget { child: Container( width: double.infinity, padding: const EdgeInsets.all(16.0), - margin: const EdgeInsets.only(left: 6.0, right: 6.0, top: 12, bottom: 6), + margin: const EdgeInsets.only(left: 8.0, right: 8.0, top: 12, bottom: 6), decoration: BoxDecoration( color: Colors.grey.shade900, borderRadius: BorderRadius.circular(16.0), From d70a5452b46dd9b8e79aa46bb3de4d3dc0ba2ef5 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Thu, 14 Nov 2024 19:31:00 +0530 Subject: [PATCH 10/14] manage and create page improvements --- app/lib/pages/apps/manage_create_page.dart | 95 +++++++++++++++++----- 1 file changed, 74 insertions(+), 21 deletions(-) diff --git a/app/lib/pages/apps/manage_create_page.dart b/app/lib/pages/apps/manage_create_page.dart index 72d1b6604..793676f79 100644 --- a/app/lib/pages/apps/manage_create_page.dart +++ b/app/lib/pages/apps/manage_create_page.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:friend_private/backend/schema/app.dart'; +import 'package:friend_private/pages/apps/add_app.dart'; import 'package:friend_private/pages/apps/list_item.dart'; import 'package:friend_private/providers/app_provider.dart'; +import 'package:friend_private/utils/other/temp.dart'; import 'package:provider/provider.dart'; class ManageCreatePage extends StatelessWidget { @@ -66,41 +68,92 @@ class ManageCreatePage extends StatelessWidget { selector: (context, provider) => provider.apps.where((p) => p.enabled).toList(), builder: (context, memoryPromptApps, child) { return ListView.builder( - itemCount: memoryPromptApps.length, - shrinkWrap: true, - itemBuilder: (context, index) { - return AppListItem( - app: memoryPromptApps[index], - index: index, - ); - }); + itemCount: memoryPromptApps.length, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, index) { + return AppListItem( + app: memoryPromptApps[index], + index: index, + ); + }, + ); }, ), + SizedBox( + height: 50, + ), ], ) : Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Text('Private Apps (${provider.userPrivateApps.length})', - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w400)), + GestureDetector( + onTap: () { + routeToPage(context, const AddAppPage()); + }, + child: Container( + padding: const EdgeInsets.all(12.0), + margin: const EdgeInsets.only(left: 12.0, right: 12.0, top: 2, bottom: 24), + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(16.0), + ), + child: const ListTile( + title: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.add, color: Colors.white), + SizedBox(width: 8), + Text( + 'Create and submit a new app', + textAlign: TextAlign.center, + ), + ], + ), + ), + ), ), - Selector>( - selector: (context, provider) => provider.apps.where((p) => p.enabled).toList(), - builder: (context, memoryPromptApps, child) { - return ListView.builder( - itemCount: memoryPromptApps.length, + provider.userPrivateApps.isEmpty + ? const SizedBox() + : Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Text('Private Apps (${provider.userPrivateApps.length})', + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w400)), + ), + provider.userPrivateApps.isEmpty + ? const SizedBox() + : ListView.builder( + itemCount: provider.userPrivateApps.length, shrinkWrap: true, itemBuilder: (context, index) { return AppListItem( - app: memoryPromptApps[index], + app: provider.userPrivateApps[index], index: index, ); - }); - }, - ), + }, + ), + provider.userPublicApps.isEmpty + ? const SizedBox() + : Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Text('Public Apps (${provider.userPublicApps.length})', + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w400)), + ), + provider.userPublicApps.isEmpty + ? const SizedBox() + : ListView.builder( + itemCount: provider.userPublicApps.length, + shrinkWrap: true, + itemBuilder: (context, index) { + return AppListItem( + app: provider.userPublicApps[index], + index: index, + ); + }, + ), ], ), ), From 29f04c7c546c60803a991f9d36abbe7342765ed3 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Thu, 14 Nov 2024 19:40:59 +0530 Subject: [PATCH 11/14] minor ui improvements --- app/lib/pages/apps/list_item.dart | 41 ++++++++++------------ app/lib/pages/apps/manage_create_page.dart | 7 ++-- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/app/lib/pages/apps/list_item.dart b/app/lib/pages/apps/list_item.dart index 07fda12b6..88b95ef4c 100644 --- a/app/lib/pages/apps/list_item.dart +++ b/app/lib/pages/apps/list_item.dart @@ -13,8 +13,9 @@ import 'app_detail.dart'; class AppListItem extends StatelessWidget { final App app; final int index; + final bool showPrivateIcon; - const AppListItem({super.key, required this.app, required this.index}); + const AppListItem({super.key, required this.app, required this.index, this.showPrivateIcon = true}); @override Widget build(BuildContext context) { @@ -93,31 +94,27 @@ class AppListItem extends StatelessWidget { ], ) : const SizedBox(), - app.installs > 0 - ? Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(Icons.download_rounded, size: 16, color: Colors.grey.shade300), - const SizedBox(width: 4), - Text('${app.installs}'), - ], - ) - : Container(), ], ), ) : Container(), - (app.ratingAvg != null || app.installs > 0) ? const SizedBox(width: 16) : const SizedBox(), - app.private - ? const Padding( - padding: EdgeInsets.only(top: 8), - child: Row( - children: [ - Icon(Icons.lock, color: Colors.grey, size: 16), - SizedBox(width: 4), - Text('Private', style: TextStyle(color: Colors.grey, fontSize: 14)), - ], - ), + app.private && showPrivateIcon + ? Row( + children: [ + (app.ratingAvg != null || app.installs > 0) + ? const SizedBox(width: 16) + : const SizedBox(), + const Padding( + padding: EdgeInsets.only(top: 8), + child: Row( + children: [ + Icon(Icons.lock, color: Colors.grey, size: 16), + SizedBox(width: 4), + Text('Private', style: TextStyle(color: Colors.grey, fontSize: 14)), + ], + ), + ), + ], ) : const SizedBox(), ], diff --git a/app/lib/pages/apps/manage_create_page.dart b/app/lib/pages/apps/manage_create_page.dart index 793676f79..853769732 100644 --- a/app/lib/pages/apps/manage_create_page.dart +++ b/app/lib/pages/apps/manage_create_page.dart @@ -80,7 +80,7 @@ class ManageCreatePage extends StatelessWidget { ); }, ), - SizedBox( + const SizedBox( height: 50, ), ], @@ -121,7 +121,7 @@ class ManageCreatePage extends StatelessWidget { : Padding( padding: const EdgeInsets.only(left: 16.0), child: Text('Private Apps (${provider.userPrivateApps.length})', - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w400)), + style: const TextStyle(fontSize: 18)), ), provider.userPrivateApps.isEmpty ? const SizedBox() @@ -130,6 +130,7 @@ class ManageCreatePage extends StatelessWidget { shrinkWrap: true, itemBuilder: (context, index) { return AppListItem( + showPrivateIcon: false, app: provider.userPrivateApps[index], index: index, ); @@ -140,7 +141,7 @@ class ManageCreatePage extends StatelessWidget { : Padding( padding: const EdgeInsets.only(left: 16.0), child: Text('Public Apps (${provider.userPublicApps.length})', - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w400)), + style: const TextStyle(fontSize: 18)), ), provider.userPublicApps.isEmpty ? const SizedBox() From 296244d67d7a6e1221acfefb1ed91e4f99fadd7c Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Fri, 15 Nov 2024 11:02:53 +0530 Subject: [PATCH 12/14] minor bug fixes --- app/lib/pages/apps/manage_create_page.dart | 8 ++++---- app/lib/pages/apps/widgets/app_section_card.dart | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/lib/pages/apps/manage_create_page.dart b/app/lib/pages/apps/manage_create_page.dart index 853769732..c78e63810 100644 --- a/app/lib/pages/apps/manage_create_page.dart +++ b/app/lib/pages/apps/manage_create_page.dart @@ -119,7 +119,7 @@ class ManageCreatePage extends StatelessWidget { provider.userPrivateApps.isEmpty ? const SizedBox() : Padding( - padding: const EdgeInsets.only(left: 16.0), + padding: const EdgeInsets.only(left: 16.0, bottom: 10), child: Text('Private Apps (${provider.userPrivateApps.length})', style: const TextStyle(fontSize: 18)), ), @@ -132,14 +132,14 @@ class ManageCreatePage extends StatelessWidget { return AppListItem( showPrivateIcon: false, app: provider.userPrivateApps[index], - index: index, + index: provider.apps.indexOf(provider.userPrivateApps[index]), ); }, ), provider.userPublicApps.isEmpty ? const SizedBox() : Padding( - padding: const EdgeInsets.only(left: 16.0), + padding: const EdgeInsets.only(left: 16.0, bottom: 10), child: Text('Public Apps (${provider.userPublicApps.length})', style: const TextStyle(fontSize: 18)), ), @@ -151,7 +151,7 @@ class ManageCreatePage extends StatelessWidget { itemBuilder: (context, index) { return AppListItem( app: provider.userPublicApps[index], - index: index, + index: provider.apps.indexOf(provider.userPublicApps[index]), ); }, ), diff --git a/app/lib/pages/apps/widgets/app_section_card.dart b/app/lib/pages/apps/widgets/app_section_card.dart index a272b61fd..c17ac4ec7 100644 --- a/app/lib/pages/apps/widgets/app_section_card.dart +++ b/app/lib/pages/apps/widgets/app_section_card.dart @@ -16,6 +16,9 @@ class AppSectionCard extends StatelessWidget { @override Widget build(BuildContext context) { + if (apps.isEmpty) { + return const SizedBox.shrink(); + } return Container( padding: const EdgeInsets.only(top: 16.0, left: 10, right: 10), height: height ?? MediaQuery.sizeOf(context).height * 0.4, From 6f63745437f12f97c2bfc1883251f9230405d0dc Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Fri, 15 Nov 2024 11:35:48 +0530 Subject: [PATCH 13/14] remove unused code and improve ratings filter --- app/lib/pages/apps/page.dart | 252 +------------------ app/lib/pages/apps/widgets/filter_sheet.dart | 18 +- app/lib/providers/app_provider.dart | 3 +- 3 files changed, 20 insertions(+), 253 deletions(-) diff --git a/app/lib/pages/apps/page.dart b/app/lib/pages/apps/page.dart index 43dd321e7..237df96e2 100644 --- a/app/lib/pages/apps/page.dart +++ b/app/lib/pages/apps/page.dart @@ -1,16 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:friend_private/backend/schema/app.dart'; -import 'package:friend_private/pages/apps/add_app.dart'; import 'package:friend_private/pages/apps/explore_install_page.dart'; -import 'package:friend_private/pages/apps/list_item.dart'; import 'package:friend_private/pages/apps/manage_create_page.dart'; import 'package:friend_private/pages/apps/providers/add_app_provider.dart'; -import 'package:friend_private/pages/chat/widgets/animated_mini_banner.dart'; import 'package:friend_private/providers/connectivity_provider.dart'; import 'package:friend_private/providers/app_provider.dart'; -import 'package:friend_private/utils/analytics/mixpanel.dart'; -import 'package:friend_private/utils/other/temp.dart'; -import 'package:friend_private/widgets/dialog.dart'; import 'package:provider/provider.dart'; class AppsPage extends StatefulWidget { @@ -34,7 +27,15 @@ class _AppsPageState extends State { Widget build(BuildContext context) { return Scaffold( backgroundColor: Theme.of(context).colorScheme.primary, - appBar: null, + appBar: widget.filterChatOnly + ? AppBar( + backgroundColor: Theme.of(context).colorScheme.primary, + automaticallyImplyLeading: true, + title: const Text('Apps'), + centerTitle: true, + elevation: 0, + ) + : null, body: DefaultTabController( length: 2, initialIndex: 0, @@ -67,195 +68,6 @@ class _AppsPageState extends State { } } -class AppsPage2 extends StatefulWidget { - final bool filterChatOnly; - - const AppsPage2({super.key, this.filterChatOnly = false}); - - @override - State createState() => _AppsPage2State(); -} - -class _AppsPage2State extends State with AutomaticKeepAliveClientMixin { - @override - void initState() { - WidgetsBinding.instance.addPostFrameCallback((_) { - context.read().initialize(widget.filterChatOnly); - context.read().getAppCapabilities(); - }); - super.initState(); - } - - @override - Widget build(BuildContext context) { - super.build(context); - return Scaffold( - backgroundColor: Theme.of(context).colorScheme.primary, - appBar: widget.filterChatOnly - ? AppBar( - backgroundColor: Theme.of(context).colorScheme.primary, - automaticallyImplyLeading: true, - title: const Text('Apps'), - centerTitle: true, - elevation: 0, - ) - : null, - body: context.read().loading - ? const Center( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - SizedBox( - height: 14, - ), - Text( - 'Loading apps', - style: TextStyle(color: Colors.white), - ), - ], - ), - ) - : GestureDetector( - onTap: () => FocusScope.of(context).unfocus(), - child: DefaultTabController( - length: 2, - initialIndex: widget.filterChatOnly ? 1 : 0, - child: Column( - children: [ - TabBar( - indicatorSize: TabBarIndicatorSize.label, - isScrollable: false, - padding: EdgeInsets.zero, - indicatorPadding: EdgeInsets.zero, - labelStyle: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 18), - indicatorColor: Colors.transparent, - tabs: const [Tab(text: 'Memories'), Tab(text: 'Chat')], - ), - InkWell( - onTap: () { - MixpanelManager().pageOpened('Submit App'); - routeToPage(context, const AddAppPage()); - }, - child: AnimatedMiniBanner( - showAppBar: true, - height: 10, - child: Container( - color: Colors.grey[800], - child: const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('Create your own app', style: TextStyle(color: Colors.white, fontSize: 16)), - ], - ), - )), - ), - Expanded( - child: TabBarView(children: [ - CustomScrollView( - slivers: [ - const EmptyAppsWidget(), - const SectionTitleWidget( - title: 'External Apps', - explainer: - 'When a memory gets created you can use these apps to send data to external apps like Notion, Zapier, and more.', - emoji: '🚀', - ), - Selector>( - selector: (context, provider) => - provider.apps.where((p) => p.worksExternally()).toList(), - builder: (context, memoryIntegrationApps, child) { - return SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - return AppListItem( - app: memoryIntegrationApps[index], - index: index, - ); - }, - childCount: memoryIntegrationApps.length, - ), - ); - }), - context.read().apps.isNotEmpty - ? SliverToBoxAdapter(child: Divider(color: Colors.grey.shade800, thickness: 1)) - : const SliverToBoxAdapter(child: SizedBox.shrink()), - const SectionTitleWidget( - title: 'Prompts', - explainer: - 'When a memory gets created you can use these apps to extract more information about each memory.', - emoji: '📝', - ), - Selector>( - selector: (context, provider) => - provider.apps.where((p) => p.worksWithMemories()).toList(), - builder: (context, memoryPromptApps, child) { - return SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - return AppListItem( - app: memoryPromptApps[index], - index: index, - ); - }, - childCount: memoryPromptApps.length, - ), - ); - }), - const SliverToBoxAdapter( - child: SizedBox( - height: 120, - ), - ), - ], - ), - CustomScrollView( - slivers: [ - const EmptyAppsWidget(), - const SectionTitleWidget( - title: 'Personalities', - explainer: 'Personalities for your chat.', - emoji: '🤖', - ), - Selector>( - selector: (context, provider) => - provider.apps.where((p) => p.worksWithChat()).toList(), - builder: (context, chatPromptApps, child) { - return SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - return AppListItem( - app: chatPromptApps[index], - index: index, - ); - }, - childCount: chatPromptApps.length, - ), - ); - }), - const SliverToBoxAdapter( - child: SizedBox( - height: 120, - ), - ), - ], - ), - ]), - ) - ], - )), - ), - ); - } - - @override - bool get wantKeepAlive => true; -} - class EmptyAppsWidget extends StatelessWidget { const EmptyAppsWidget({super.key}); @@ -281,49 +93,3 @@ class EmptyAppsWidget extends StatelessWidget { }); } } - -class SectionTitleWidget extends StatelessWidget { - final String title; - final String emoji; - final String explainer; - const SectionTitleWidget({super.key, required this.title, required this.emoji, required this.explainer}); - - @override - Widget build(BuildContext context) { - return Consumer(builder: (context, provider, child) { - return provider.apps.isNotEmpty - ? SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(top: 32), - child: GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (c) => getDialog( - context, - () => Navigator.pop(context), - () => Navigator.pop(context), - '$title $emoji', - explainer, - singleButton: true, - okButtonText: 'Got it!', - ), - ); - }, - child: Center( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(title, style: const TextStyle(color: Colors.white, fontSize: 18)), - const SizedBox(width: 12), - Text(emoji, style: const TextStyle(fontSize: 18)), - ], - ), - ), - ), - ), - ) - : const SliverToBoxAdapter(child: SizedBox.shrink()); - }); - } -} diff --git a/app/lib/pages/apps/widgets/filter_sheet.dart b/app/lib/pages/apps/widgets/filter_sheet.dart index 0a3c1e281..04a78f1eb 100644 --- a/app/lib/pages/apps/widgets/filter_sheet.dart +++ b/app/lib/pages/apps/widgets/filter_sheet.dart @@ -95,29 +95,29 @@ class FilterBottomSheet extends StatelessWidget { child: Column( children: [ FilterOption( - label: '1+ Star', + label: '1+ Stars', onTap: () { - provider.addOrRemoveFilter('1+', 'Rating'); + provider.addOrRemoveFilter('1+ Stars', 'Rating'); }, - isSelected: provider.isFilterSelected('1+', 'Rating')), + isSelected: provider.isFilterSelected('1+ Stars', 'Rating')), FilterOption( label: '2+ Stars', onTap: () { - provider.addOrRemoveFilter('2+', 'Rating'); + provider.addOrRemoveFilter('2+ Stars', 'Rating'); }, - isSelected: provider.isFilterSelected('2+', 'Rating')), + isSelected: provider.isFilterSelected('2+ Stars', 'Rating')), FilterOption( label: '3+ Stars', onTap: () { - provider.addOrRemoveFilter('3+', 'Rating'); + provider.addOrRemoveFilter('3+ Stars', 'Rating'); }, - isSelected: provider.isFilterSelected('3+', 'Rating')), + isSelected: provider.isFilterSelected('3+ Stars', 'Rating')), FilterOption( label: '4+ Stars', onTap: () { - provider.addOrRemoveFilter('4+', 'Rating'); + provider.addOrRemoveFilter('4+ Stars', 'Rating'); }, - isSelected: provider.isFilterSelected('4+', 'Rating')), + isSelected: provider.isFilterSelected('4+ Stars', 'Rating')), ], ), ), diff --git a/app/lib/providers/app_provider.dart b/app/lib/providers/app_provider.dart index b50e4ccc1..9d8b295f6 100644 --- a/app/lib/providers/app_provider.dart +++ b/app/lib/providers/app_provider.dart @@ -131,7 +131,8 @@ class AppProvider extends BaseProvider { filteredApps = filteredApps.where((app) => app.category == (value as Category).id).toList(); break; case 'Rating': - value = value.replaceAll('+', ''); + value = value as String; + value = value.replaceAll('+ Stars', ''); filteredApps = filteredApps.where((app) => (app.ratingAvg ?? 0.0) >= double.parse(value)).toList(); break; case 'Capabilities': From adb7df7e78572945929fdc5ba55c9a2ee9344901 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Fri, 15 Nov 2024 11:49:00 +0530 Subject: [PATCH 14/14] preserve state and minor changes --- app/lib/pages/apps/explore_install_page.dart | 40 +++++++++++++++----- app/lib/pages/apps/manage_create_page.dart | 2 + 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/app/lib/pages/apps/explore_install_page.dart b/app/lib/pages/apps/explore_install_page.dart index 16cd7496d..f76b474bc 100644 --- a/app/lib/pages/apps/explore_install_page.dart +++ b/app/lib/pages/apps/explore_install_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:friend_private/backend/schema/app.dart'; +import 'package:friend_private/pages/apps/providers/add_app_provider.dart'; import 'package:friend_private/pages/apps/widgets/app_section_card.dart'; import 'package:friend_private/pages/apps/widgets/filter_sheet.dart'; import 'package:friend_private/pages/apps/list_item.dart'; @@ -25,12 +26,15 @@ class ExploreInstallPage extends StatefulWidget { State createState() => _ExploreInstallPageState(); } -class _ExploreInstallPageState extends State { +class _ExploreInstallPageState extends State with AutomaticKeepAliveClientMixin { late TextEditingController searchController; @override void initState() { searchController = TextEditingController(); + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().init(); + }); super.initState(); } @@ -42,6 +46,7 @@ class _ExploreInstallPageState extends State { @override Widget build(BuildContext context) { + super.build(context); return Consumer(builder: (context, provider, child) { return CustomScrollView( slivers: [ @@ -197,15 +202,27 @@ class _ExploreInstallPageState extends State { ), ); } - return SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - return AppListItem( - app: provider.filteredApps[index], - index: index, - ); - }, - childCount: provider.filteredApps.length, + return SliverToBoxAdapter( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListView.separated( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemBuilder: (ctx, idx) { + return AppListItem( + app: provider.filteredApps[idx], + index: + provider.apps.indexWhere((element) => element.id == provider.filteredApps[idx].id), + ); + }, + separatorBuilder: (ctx, idx) { + return const SizedBox(height: 8); + }, + itemCount: provider.filteredApps.length, + ), + const SizedBox(height: 64), + ], ), ); }, @@ -252,4 +269,7 @@ class _ExploreInstallPageState extends State { ); }); } + + @override + bool get wantKeepAlive => true; } diff --git a/app/lib/pages/apps/manage_create_page.dart b/app/lib/pages/apps/manage_create_page.dart index c78e63810..6247bd6b2 100644 --- a/app/lib/pages/apps/manage_create_page.dart +++ b/app/lib/pages/apps/manage_create_page.dart @@ -3,6 +3,7 @@ import 'package:friend_private/backend/schema/app.dart'; import 'package:friend_private/pages/apps/add_app.dart'; import 'package:friend_private/pages/apps/list_item.dart'; import 'package:friend_private/providers/app_provider.dart'; +import 'package:friend_private/utils/analytics/mixpanel.dart'; import 'package:friend_private/utils/other/temp.dart'; import 'package:provider/provider.dart'; @@ -91,6 +92,7 @@ class ManageCreatePage extends StatelessWidget { const SizedBox(height: 16), GestureDetector( onTap: () { + MixpanelManager().pageOpened('Submit App'); routeToPage(context, const AddAppPage()); }, child: Container(