diff --git a/app/lib/pages/apps/app_detail/app_detail.dart b/app/lib/pages/apps/app_detail/app_detail.dart index 972621742e..62d26dacf5 100644 --- a/app/lib/pages/apps/app_detail/app_detail.dart +++ b/app/lib/pages/apps/app_detail/app_detail.dart @@ -11,7 +11,7 @@ import 'package:omi/backend/http/api/apps.dart'; import 'package:omi/backend/preferences.dart'; import 'package:omi/pages/apps/app_detail/reviews_list_page.dart'; import 'package:omi/pages/apps/markdown_viewer.dart'; -import 'package:omi/pages/chat/page.dart'; +import 'package:omi/providers/home_provider.dart'; import 'package:omi/pages/apps/providers/add_app_provider.dart'; import 'package:omi/providers/app_provider.dart'; import 'package:omi/providers/message_provider.dart'; @@ -677,12 +677,8 @@ class _AppDetailPageState extends State { // Navigate directly to chat page if (mounted) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const ChatPage(isPivotBottom: false), - ), - ); + Navigator.popUntil(context, (route) => route.isFirst); + Provider.of(context, listen: false).setIndex(2); } } finally { if (mounted) { diff --git a/app/lib/pages/chat/page.dart b/app/lib/pages/chat/page.dart index 2a52fa1a08..1c28b4172c 100644 --- a/app/lib/pages/chat/page.dart +++ b/app/lib/pages/chat/page.dart @@ -1,3 +1,4 @@ +import 'dart:math' as math; import 'package:cached_network_image/cached_network_image.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -22,6 +23,7 @@ import 'package:omi/providers/home_provider.dart'; import 'package:omi/providers/conversation_provider.dart'; import 'package:omi/providers/message_provider.dart'; import 'package:omi/providers/app_provider.dart'; +import 'package:omi/providers/device_provider.dart'; import 'package:omi/utils/alerts/app_snackbar.dart'; import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/other/temp.dart'; @@ -36,11 +38,13 @@ import 'widgets/message_action_menu.dart'; class ChatPage extends StatefulWidget { final bool isPivotBottom; final String? autoMessage; + final bool isEmbeddedInTab; const ChatPage({ super.key, this.isPivotBottom = false, this.autoMessage, + this.isEmbeddedInTab = false, }); @override @@ -61,7 +65,7 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin, var prefs = SharedPreferencesUtil(); late List apps; - final scaffoldKey = GlobalKey(); + final GlobalKey scaffoldKey = GlobalKey(); // Track which app is pending deletion confirmation String? _pendingDeleteAppId; @@ -69,6 +73,10 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin, @override bool get wantKeepAlive => true; + void openEndDrawer() { + scaffoldKey.currentState?.openEndDrawer(); + } + @override void initState() { apps = prefs.appsList; @@ -167,7 +175,7 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin, key: scaffoldKey, resizeToAvoidBottomInset: false, backgroundColor: Theme.of(context).colorScheme.primary, - appBar: _buildAppBar(context, provider), + appBar: widget.isEmbeddedInTab ? null : _buildAppBar(context, provider), endDrawer: _buildChatAppsEndDrawer(context), onEndDrawerChanged: (isOpened) { if (isOpened) { @@ -384,7 +392,10 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin, ), // Send message area - fixed at bottom Container( - margin: EdgeInsets.only(top: 10, bottom: MediaQuery.of(context).viewInsets.bottom), + margin: EdgeInsets.only( + top: 10, + bottom: widget.isEmbeddedInTab ? 0 : MediaQuery.of(context).viewInsets.bottom, + ), decoration: const BoxDecoration( color: Colors.transparent, borderRadius: BorderRadius.only( @@ -495,20 +506,32 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin, } }), // Send bar - Padding( - padding: EdgeInsets.only( - left: 8, - right: 8, - top: provider.selectedFiles.isNotEmpty ? 0 : 8, - bottom: widget.isPivotBottom ? 20 : (textFieldFocusNode.hasFocus ? 10 : 40), - ), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), - decoration: BoxDecoration( - color: const Color(0xFF2A2A2F), - borderRadius: BorderRadius.circular(32), - ), - child: Row( + Consumer( + builder: (context, deviceProvider, child) { + final bool hasDevice = deviceProvider.isConnected && deviceProvider.connectedDevice != null; + final double embeddedBottomPadding = hasDevice ? 100 : 130; + final double keyboardHeight = MediaQuery.of(context).viewInsets.bottom; + + return AnimatedPadding( + duration: widget.isEmbeddedInTab ? Duration.zero : const Duration(milliseconds: 200), + curve: Curves.easeOut, + padding: EdgeInsets.only( + left: 8, + right: 8, + top: provider.selectedFiles.isNotEmpty ? 0 : 8, + bottom: widget.isPivotBottom + ? 20 + : (widget.isEmbeddedInTab + ? math.max(embeddedBottomPadding, 10 + keyboardHeight) + : (keyboardHeight > 0 ? 10 : 40)), + ), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration( + color: const Color(0xFF2A2A2F), + borderRadius: BorderRadius.circular(32), + ), + child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ // Plus button @@ -648,7 +671,9 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin, ], ), ), - ), + ); + }, + ), ], ); }), @@ -708,6 +733,40 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin, scrollToBottom() => _moveListToBottom(); + void addAutoMessage(String message) { + if (message.isEmpty || !mounted) return; + + final aiMessage = ServerMessage( + const Uuid().v4(), + DateTime.now(), + message, + MessageSender.ai, + MessageType.text, + null, + false, + [], + [], + [], + askForNps: false, + ); + context.read().addMessage(aiMessage); + Future.delayed(const Duration(milliseconds: 100), () { + if (mounted) scrollToBottom(); + }); + } + + void handleAppSelection(String? val, AppProvider provider) { + _handleAppSelection(val, provider); + } + + void navigateToChatAppsPage() { + _navigateToChatAppsPage(); + } + + Future handleAppUninstall(String appId, AppProvider appProvider, MessageProvider messageProvider) async { + await _handleAppUninstall(appId, appProvider, messageProvider); + } + void _handleAppSelection(String? val, AppProvider provider) { if (val == null || val == provider.selectedChatAppId) { return; @@ -758,7 +817,7 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin, await routeToPage( context, CapabilityAppsPage( - capability: AppCapability(id: 'chat', title: 'Chat Assistants'), + capability: AppCapability(id: 'chat', title: 'Chat Apps'), apps: const [], ), ); @@ -984,7 +1043,7 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin, child: FaIcon(FontAwesomeIcons.circlePlus, color: Colors.white, size: 20), ), title: const Text( - 'Enable Apps', + 'Get More Chat Apps', style: TextStyle(color: Colors.white, fontSize: 16), ), trailing: const Padding( @@ -1000,7 +1059,7 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin, const Padding( padding: EdgeInsets.fromLTRB(16, 16, 20, 8), child: Text( - 'Select App', + 'Select Chat App', style: TextStyle( color: Colors.white60, fontSize: 13, @@ -1037,15 +1096,6 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin, ? () => _handleAppUninstall(app.id, appProvider, messageProvider) : null, )), - if (chatApps.isEmpty) - const Padding( - padding: EdgeInsets.all(20), - child: Text( - 'No chat apps enabled.\nTap "Enable Apps" to add some.', - style: TextStyle(color: Colors.white38, fontSize: 14), - textAlign: TextAlign.center, - ), - ), ], ), ), diff --git a/app/lib/pages/conversations/conversations_page.dart b/app/lib/pages/conversations/conversations_page.dart index fd0480960c..d14424aab9 100644 --- a/app/lib/pages/conversations/conversations_page.dart +++ b/app/lib/pages/conversations/conversations_page.dart @@ -188,7 +188,15 @@ class _ConversationsPageState extends State with AutomaticKee const SliverToBoxAdapter(child: SearchResultHeaderWidget()), getProcessingConversationsWidget(convoProvider.processingConversations), // Goal tracker widget - before folders - const SliverToBoxAdapter(child: GoalTrackerWidget()), + Consumer2( + builder: (context, homeProvider, convoProvider, _) { + bool isSearching = homeProvider.showConvoSearchBar || convoProvider.previousQuery.isNotEmpty; + if (isSearching) { + return const SliverToBoxAdapter(child: SizedBox.shrink()); + } + return const SliverToBoxAdapter(child: GoalTrackerWidget()); + }, + ), // Folder tabs Consumer2( builder: (context, folderProvider, convoProvider, _) { diff --git a/app/lib/pages/conversations/widgets/goal_tracker_widget.dart b/app/lib/pages/conversations/widgets/goal_tracker_widget.dart index 0227539f55..524cdc1ed6 100644 --- a/app/lib/pages/conversations/widgets/goal_tracker_widget.dart +++ b/app/lib/pages/conversations/widgets/goal_tracker_widget.dart @@ -3,8 +3,12 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:omi/backend/http/api/goals.dart'; -import 'package:omi/pages/chat/page.dart'; +import 'package:omi/backend/schema/message.dart'; +import 'package:omi/providers/home_provider.dart'; +import 'package:omi/providers/message_provider.dart'; +import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:uuid/uuid.dart'; /// Goal tracker widget with semicircle gauge class GoalTrackerWidget extends StatefulWidget { @@ -22,7 +26,6 @@ class _GoalTrackerWidgetState extends State bool _isLoading = false; // Start as false - show cached data immediately bool _isEditingGoal = false; bool _isEditingValue = false; - bool _initialLoadDone = false; static const String _goalStorageKey = 'goal_tracker_local_goal'; static const String _adviceStorageKey = 'goal_tracker_local_advice'; @@ -132,7 +135,6 @@ class _GoalTrackerWidgetState extends State if (mounted) { setState(() { _isLoading = false; - _initialLoadDone = true; }); } } @@ -202,17 +204,26 @@ class _GoalTrackerWidgetState extends State void _openChatWithAdvice() { if (_advice == null || _advice!.isEmpty) return; HapticFeedback.lightImpact(); - - // Navigate to chat and send the advice as a message from Omi AI - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ChatPage( - isPivotBottom: false, - autoMessage: _advice, - ), - ), + + // Add the AI message directly to MessageProvider + final aiMessage = ServerMessage( + const Uuid().v4(), + DateTime.now(), + _advice!, + MessageSender.ai, + MessageType.text, + null, + false, + [], + [], + [], + askForNps: false, ); + Provider.of(context, listen: false).addMessage(aiMessage); + + // Navigate to chat tab + Navigator.popUntil(context, (route) => route.isFirst); + Provider.of(context, listen: false).setIndex(2); } Future _createGoalFromSuggestion() async { diff --git a/app/lib/pages/home/page.dart b/app/lib/pages/home/page.dart index 076c3b9af0..f184187a97 100644 --- a/app/lib/pages/home/page.dart +++ b/app/lib/pages/home/page.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'dart:io'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; @@ -10,6 +12,7 @@ import 'package:omi/backend/http/api/users.dart'; import 'package:omi/backend/preferences.dart'; import 'package:omi/backend/schema/app.dart'; import 'package:omi/backend/schema/geolocation.dart'; +import 'package:omi/gen/assets.gen.dart'; import 'package:omi/main.dart'; import 'package:omi/pages/action_items/action_items_page.dart'; import 'package:omi/pages/apps/app_detail/app_detail.dart'; @@ -110,9 +113,11 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker bool scriptsInProgress = false; StreamSubscription? _notificationStreamSubscription; + String? _pendingDeleteAppId; + final GlobalKey> _conversationsPageKey = GlobalKey>(); final GlobalKey> _actionItemsPageKey = GlobalKey>(); - final GlobalKey> _memoriesPageKey = GlobalKey>(); + final GlobalKey _chatPageKey = GlobalKey(); final GlobalKey _appsPageKey = GlobalKey(); late final List _pages; @@ -136,10 +141,6 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker } break; case 2: - final memoriesState = _memoriesPageKey.currentState; - if (memoriesState != null) { - (memoriesState as dynamic).scrollToTop(); - } break; case 3: final appsState = _appsPageKey.currentState; @@ -177,7 +178,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker ///Screens with respect to subpage final Map screensWithRespectToPath = { - '/facts': const MemoriesPage(), + '/facts': const MemoriesPage(showAppBar: true), }; bool? previousConnection; @@ -200,7 +201,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker _pages = [ ConversationsPage(key: _conversationsPageKey), ActionItemsPage(key: _actionItemsPageKey), - MemoriesPage(key: _memoriesPageKey), + ChatPage(key: _chatPageKey, isEmbeddedInTab: true), AppsPage(key: _appsPageKey), ]; SharedPreferencesUtil().onboardingCompleted = true; @@ -224,6 +225,9 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker switch (pageAlias) { case "memories": + homePageIdx = 2; // This will now be Chat + break; + case "chat": homePageIdx = 2; break; case "apps": @@ -289,22 +293,17 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker await Provider.of(context, listen: false).refreshMessages(); } } - // Navigate to chat page directly since it's no longer in the tab bar - // If there's an auto-message (e.g., from daily reflection notification), send it + // The tab is already set to 2 in initState, but if there's an auto-message, we might need to handle it. final autoMessageToSend = widget.autoMessage; - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ChatPage( - isPivotBottom: false, - autoMessage: autoMessageToSend, - ), - ), - ); - } - }); + if (autoMessageToSend != null && mounted) { + // We can't pass autoMessage directly to the ChatPage widget in the IndexedStack easily + // unless we use a provider or the GlobalKey. + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_chatPageKey.currentState != null) { + _chatPageKey.currentState!.addAutoMessage(autoMessageToSend); + } + }); + } break; case "settings": // Use context from the current widget instead of navigator key for bottom sheet @@ -322,9 +321,10 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker } break; case "facts": + case "memories": MyApp.navigatorKey.currentState?.push( MaterialPageRoute( - builder: (context) => const MemoriesPage(), + builder: (context) => const MemoriesPage(showAppBar: true), ), ); break; @@ -454,6 +454,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker child: Consumer( builder: (context, homeProvider, _) { return Scaffold( + resizeToAvoidBottomInset: false, backgroundColor: Theme.of(context).colorScheme.primary, appBar: homeProvider.selectedIndex == 5 ? null : _buildAppBar(context), body: DefaultTabController( @@ -513,7 +514,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker children: [ // Home tab Expanded( - child: InkWell( + child: GestureDetector( onTap: () { HapticFeedback.mediumImpact(); MixpanelManager().bottomNavigationTabClicked('Home'); @@ -538,7 +539,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker ), // Action Items tab Expanded( - child: InkWell( + child: GestureDetector( onTap: () { HapticFeedback.mediumImpact(); MixpanelManager().bottomNavigationTabClicked('Action Items'); @@ -563,15 +564,14 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker ), // Center space for record button - only when no OMI device is connected if (!isOmiDeviceConnected) const SizedBox(width: 80), - // Memories tab + // Chat tab Expanded( - child: InkWell( + child: GestureDetector( onTap: () { HapticFeedback.mediumImpact(); - MixpanelManager().bottomNavigationTabClicked('Memories'); + MixpanelManager().bottomNavigationTabClicked('Chat'); primaryFocus?.unfocus(); if (home.selectedIndex == 2) { - _scrollToTop(2); return; } home.setIndex(2); @@ -580,7 +580,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker height: 90, child: Center( child: Icon( - FontAwesomeIcons.brain, + FontAwesomeIcons.solidComment, color: home.selectedIndex == 2 ? Colors.white : Colors.grey, size: 26, ), @@ -590,7 +590,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker ), // Apps tab Expanded( - child: InkWell( + child: GestureDetector( onTap: () { HapticFeedback.mediumImpact(); MixpanelManager().bottomNavigationTabClicked('Apps'); @@ -659,51 +659,6 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker }, ), ), - // Floating Chat Button - Bottom Right (only on homepage) - if (home.selectedIndex == 0) - Positioned( - right: 20, - bottom: 100, // Position above the bottom navigation bar - child: GestureDetector( - onTap: () { - HapticFeedback.mediumImpact(); - MixpanelManager().bottomNavigationTabClicked('Chat'); - // Navigate to chat page - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const ChatPage(isPivotBottom: false), - ), - ); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(32), - color: Colors.deepPurple, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - FontAwesomeIcons.solidComment, - size: 22, - color: Colors.white, - ), - const SizedBox(width: 10), - Text( - context.l10n.askOmi, - style: const TextStyle( - color: Colors.white, - fontSize: 17, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ), - ), ], ); } @@ -759,6 +714,286 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker } } + void _showAppSelectorSheet(BuildContext context) { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (context, setModalState) { + return Container( + height: MediaQuery.of(context).size.height * 0.7, + decoration: const BoxDecoration( + color: Color(0xFF1F1F25), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: SafeArea( + child: Consumer2( + builder: (context, messageProvider, appProvider, child) { + final chatApps = messageProvider.chatApps; + final selectedAppId = appProvider.selectedChatAppId; + final isOmiSelected = chatApps.firstWhereOrNull((a) => a.id == selectedAppId) == null; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 16, 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Select Chat App', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + IconButton( + icon: const Padding( + padding: EdgeInsets.only(left: 2, top: 1), + child: FaIcon(FontAwesomeIcons.xmark, color: Colors.white60, size: 18), + ), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ), + // App list + Expanded( + child: ListView( + padding: EdgeInsets.zero, + children: [ + // Omi option + _buildAppSelectorItem( + avatar: _getOmiAvatar(), + name: 'Omi', + isSelected: isOmiSelected, + onTap: () { + Navigator.of(context).pop(); + _chatPageKey.currentState?.handleAppSelection('no_selected', appProvider); + }, + setModalState: setModalState, + ), + // Enabled chat apps + ...chatApps.map((app) => _buildAppSelectorItem( + avatar: _getAppAvatar(app), + name: app.getName(), + isSelected: selectedAppId == app.id, + appId: app.id, + onTap: () { + Navigator.of(context).pop(); + _chatPageKey.currentState?.handleAppSelection(app.id, appProvider); + }, + onConfirmDelete: selectedAppId != app.id + ? () => _chatPageKey.currentState?.handleAppUninstall(app.id, appProvider, messageProvider) + : null, + setModalState: setModalState, + )), + const Divider(color: Colors.white12, height: 1), + ListTile( + leading: const Padding( + padding: EdgeInsets.only(left: 2, top: 1), + child: FaIcon(FontAwesomeIcons.circlePlus, color: Colors.white, size: 20), + ), + title: const Text( + 'Get More Chat Apps', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + trailing: const Padding( + padding: EdgeInsets.only(left: 2, top: 1), + child: FaIcon(FontAwesomeIcons.chevronRight, color: Colors.white38, size: 14), + ), + onTap: () { + Navigator.of(context).pop(); + _chatPageKey.currentState?.navigateToChatAppsPage(); + }, + ), + ], + ), + ), + ], + ); + }, + ), + ), + ); + }, + ); + }, + ); + } + + Widget _buildAppSelectorItem({ + required Widget avatar, + required String name, + required bool isSelected, + required VoidCallback onTap, + required StateSetter setModalState, + String? appId, + VoidCallback? onConfirmDelete, + }) { + final bool isPendingDelete = appId != null && _pendingDeleteAppId == appId; + + if (isPendingDelete) { + // Show inline confirmation buttons - match ListTile height (56px) + return Container( + height: 56, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + avatar, + const SizedBox(width: 12), + Expanded( + child: Text( + name, + style: const TextStyle(color: Colors.white, fontSize: 16), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + // Cancel button (white) + GestureDetector( + onTap: () { + setModalState(() { + _pendingDeleteAppId = null; + }); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: const Text( + 'Cancel', + style: TextStyle( + color: Colors.black, + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + const SizedBox(width: 8), + // Disable button (red) + GestureDetector( + onTap: () { + setModalState(() { + _pendingDeleteAppId = null; + }); + onConfirmDelete?.call(); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.redAccent, + borderRadius: BorderRadius.circular(16), + ), + child: const Text( + 'Disable', + style: TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ); + } + + return ListTile( + leading: avatar, + title: Text( + name, + style: const TextStyle(color: Colors.white, fontSize: 16), + overflow: TextOverflow.ellipsis, + ), + trailing: isSelected + ? const Padding( + padding: EdgeInsets.only(left: 2, top: 1), + child: FaIcon(FontAwesomeIcons.solidCircleCheck, color: Colors.white, size: 18), + ) + : appId != null && onConfirmDelete != null + ? GestureDetector( + onTap: () { + setModalState(() { + _pendingDeleteAppId = appId; + }); + }, + child: const Padding( + padding: EdgeInsets.only(left: 2, top: 1), + child: FaIcon(FontAwesomeIcons.solidTrashCan, color: Colors.white38, size: 16), + ), + ) + : null, + selected: isSelected, + selectedTileColor: Colors.white.withOpacity(0.1), + onTap: onTap, + ); + } + + Widget _getAppAvatar(App app) { + return CachedNetworkImage( + imageUrl: app.getImageUrl(), + imageBuilder: (context, imageProvider) { + return CircleAvatar( + backgroundColor: Colors.white, + radius: 12, + backgroundImage: imageProvider, + ); + }, + errorWidget: (context, url, error) { + return const CircleAvatar( + backgroundColor: Colors.white, + radius: 12, + child: Icon(Icons.error_outline_rounded), + ); + }, + progressIndicatorBuilder: (context, url, progress) => CircleAvatar( + backgroundColor: Colors.white, + radius: 12, + child: CircularProgressIndicator( + value: progress.progress, + valueColor: const AlwaysStoppedAnimation(Colors.white), + ), + ), + ); + } + + Widget _getOmiAvatar() { + return Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(Assets.images.background.path), + fit: BoxFit.cover, + ), + borderRadius: const BorderRadius.all(Radius.circular(16.0)), + ), + height: 24, + width: 24, + child: Stack( + alignment: Alignment.center, + children: [ + Image.asset( + Assets.images.herologo.path, + height: 16, + width: 16, + ), + ], + ), + ); + } + PreferredSizeWidget _buildAppBar(BuildContext context) { return AppBar( automaticallyImplyLeading: false, @@ -767,8 +1002,52 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - const BatteryInfoWidget(), - const SizedBox.shrink(), + Row( + children: [ + const BatteryInfoWidget(), + Consumer( + builder: (context, homeProvider, child) { + if (homeProvider.selectedIndex == 2) { + return Consumer2( + builder: (context, appProvider, messageProvider, child) { + var selectedApp = messageProvider.chatApps + .firstWhereOrNull((app) => app.id == appProvider.selectedChatAppId); + return GestureDetector( + onTap: () { + HapticFeedback.mediumImpact(); + _showAppSelectorSheet(context); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 12), + selectedApp != null ? _getAppAvatar(selectedApp) : _getOmiAvatar(), + const SizedBox(width: 8), + Container( + constraints: const BoxConstraints(maxWidth: 120), + child: Text( + selectedApp != null ? selectedApp.getName() : "Omi", + style: const TextStyle(color: Colors.white, fontSize: 16), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 4), + const FaIcon( + FontAwesomeIcons.chevronRight, + color: Colors.white54, + size: 12, + ), + ], + ), + ); + }, + ); + } + return const SizedBox.shrink(); + }, + ), + ], + ), Row( children: [ // Sync icon for Limitless devices @@ -952,9 +1231,9 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker ), value: [selectedDate], onValueChanged: (dates) { - if (dates.isNotEmpty && dates[0] != null) { - selectedDate = dates[0]!; - } + if (dates.isNotEmpty && dates[0] != null) { + selectedDate = dates[0]!; + } }, ), ), @@ -1037,6 +1316,40 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker ); }, ), + // New Chat button - only on Chat tab + Consumer( + builder: (context, homeProvider, child) { + if (homeProvider.selectedIndex == 2) { + return Row( + children: [ + Container( + width: 36, + height: 36, + decoration: const BoxDecoration( + color: Color(0xFF1F1F25), + shape: BoxShape.circle, + ), + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon( + Icons.add_comment, + size: 18, + color: Colors.white70, + ), + onPressed: () { + HapticFeedback.mediumImpact(); + final appProvider = context.read(); + _chatPageKey.currentState?.handleAppSelection('clear_chat', appProvider); + }, + ), + ), + const SizedBox(width: 8), + ], + ); + } + return const SizedBox.shrink(); + }, + ), // Settings button - always visible Container( width: 36, diff --git a/app/lib/pages/home/widgets/chat_apps_dropdown_widget.dart b/app/lib/pages/home/widgets/chat_apps_dropdown_widget.dart index 7ad446cf48..b6c7395498 100644 --- a/app/lib/pages/home/widgets/chat_apps_dropdown_widget.dart +++ b/app/lib/pages/home/widgets/chat_apps_dropdown_widget.dart @@ -318,7 +318,7 @@ class ChatAppsDropdownWidget extends StatelessWidget { mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Enable Apps', style: TextStyle(color: Colors.white, fontSize: 16)), + Text('Get More Chat Apps', style: TextStyle(color: Colors.white, fontSize: 16)), SizedBox( width: 24, child: Icon(Icons.apps, color: Colors.white60, size: 16), diff --git a/app/lib/pages/memories/page.dart b/app/lib/pages/memories/page.dart index ab4d019cc1..3bd9626963 100644 --- a/app/lib/pages/memories/page.dart +++ b/app/lib/pages/memories/page.dart @@ -20,7 +20,8 @@ import 'widgets/memory_management_sheet.dart'; import 'widgets/memory_graph_page.dart'; class MemoriesPage extends StatefulWidget { - const MemoriesPage({super.key}); + final bool showAppBar; + const MemoriesPage({super.key, this.showAppBar = false}); @override State createState() => MemoriesPageState(); @@ -153,6 +154,25 @@ class MemoriesPageState extends State with AutomaticKeepAliveClien canPop: true, child: Scaffold( backgroundColor: Theme.of(context).colorScheme.primary, + appBar: widget.showAppBar + ? AppBar( + title: Text( + context.l10n.memories, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + centerTitle: true, + backgroundColor: Theme.of(context).colorScheme.primary, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white, size: 20), + onPressed: () => Navigator.of(context).pop(), + ), + ) + : null, floatingActionButton: Padding( padding: const EdgeInsets.only(bottom: 60.0), child: FloatingActionButton( diff --git a/app/lib/pages/settings/profile.dart b/app/lib/pages/settings/profile.dart index 9827833c2c..4c615443de 100644 --- a/app/lib/pages/settings/profile.dart +++ b/app/lib/pages/settings/profile.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:omi/backend/preferences.dart'; +import 'package:omi/pages/memories/page.dart'; import 'package:omi/pages/payments/payments_page.dart'; import 'package:omi/pages/settings/change_name_widget.dart'; import 'package:omi/pages/settings/language_settings_page.dart'; @@ -242,6 +243,15 @@ class _ProfilePageState extends State { routeToPage(context, const UserPeoplePage()); }, ), + const Divider(height: 1, color: Color(0xFF3C3C43)), + _buildProfileItem( + title: context.l10n.memories, + icon: const FaIcon(FontAwesomeIcons.brain, color: Color(0xFF8E8E93), size: 20), + onTap: () { + routeToPage(context, const MemoriesPage(showAppBar: true)); + MixpanelManager().pageOpened('Memories'); + }, + ), ], ), const SizedBox(height: 32),